mod.rs
1 use crate::error::AbundantisError; 2 use crate::Result; 3 use compact_str::CompactString; 4 use dashmap::DashMap; 5 use lru::LruCache; 6 use parking_lot::RwLock; 7 use std::collections::{HashMap, HashSet}; 8 use std::num::NonZeroUsize; 9 use std::sync::atomic::{AtomicU64, Ordering}; 10 use std::sync::Arc; 11 use std::time::{Duration, Instant}; 12 13 #[cfg(feature = "async")] 14 use maybe_async::must_be_async; 15 #[cfg(not(feature = "async"))] 16 use maybe_async::must_be_sync; 17 18 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] 19 pub struct CacheKey { 20 pub key: CompactString, 21 pub context_hash: u64, 22 } 23 24 impl CacheKey { 25 pub fn new(key: impl Into<CompactString>, context_hash: u64) -> Self { 26 Self { 27 key: key.into(), 28 context_hash, 29 } 30 } 31 } 32 33 #[derive(Debug, Clone)] 34 pub struct CachedValue { 35 pub value: Arc<ResolvedVariable>, 36 pub cached_at: Instant, 37 } 38 39 #[derive(Debug, Clone)] 40 pub struct ResolvedVariable { 41 pub key: CompactString, 42 pub raw_value: CompactString, 43 pub resolved_value: CompactString, 44 pub source: super::source::VariableSource, 45 pub description: Option<CompactString>, 46 pub has_warnings: bool, 47 pub interpolation_depth: u32, 48 } 49 50 #[derive(Debug, Clone)] 51 pub struct DependencyEdge { 52 pub from: CompactString, 53 pub to: CompactString, 54 pub span: Option<(u32, u32)>, 55 } 56 57 #[derive(Debug, Clone)] 58 pub struct DependencyGraph { 59 edges: Vec<DependencyEdge>, 60 nodes: HashMap<CompactString, Vec<DependencyEdge>>, 61 } 62 63 impl DependencyGraph { 64 pub fn new() -> Self { 65 Self { 66 edges: Vec::new(), 67 nodes: HashMap::new(), 68 } 69 } 70 71 pub fn add_edge(&mut self, from: CompactString, to: CompactString, span: Option<(u32, u32)>) { 72 let edge = DependencyEdge { 73 from: from.clone(), 74 to: to.clone(), 75 span, 76 }; 77 self.edges.push(edge.clone()); 78 self.nodes.entry(from).or_default().push(edge); 79 } 80 81 pub fn detect_cycle(&self, start: &str) -> Vec<CompactString> { 82 let mut visited = HashMap::new(); 83 let mut path = Vec::new(); 84 self.detect_cycle_with_state(start, &mut visited, &mut path) 85 } 86 87 pub fn detect_cycle_with_state( 88 &self, 89 start: &str, 90 visited: &mut HashMap<CompactString, bool>, 91 path: &mut Vec<CompactString>, 92 ) -> Vec<CompactString> { 93 visited.clear(); 94 path.clear(); 95 96 if self.dfs_detect_cycle(start, visited, path) { 97 std::mem::take(path) 98 } else { 99 Vec::new() 100 } 101 } 102 103 fn dfs_detect_cycle( 104 &self, 105 current: &str, 106 visited: &mut HashMap<CompactString, bool>, 107 path: &mut Vec<CompactString>, 108 ) -> bool { 109 if let Some(&in_path) = visited.get(current) { 110 if in_path { 111 return true; 112 } 113 return false; 114 } 115 116 visited.insert(CompactString::new(current), true); 117 path.push(CompactString::new(current)); 118 119 if let Some(edges) = self.nodes.get(current) { 120 for edge in edges { 121 if self.dfs_detect_cycle(&edge.to, visited, path) { 122 return true; 123 } 124 } 125 } 126 127 path.pop(); 128 visited.insert(CompactString::new(current), false); 129 false 130 } 131 132 pub fn get_dependencies(&self, key: &str) -> Vec<CompactString> { 133 self.nodes 134 .get(key) 135 .map(|edges| edges.iter().map(|e| e.to.clone()).collect()) 136 .unwrap_or_default() 137 } 138 139 pub fn clear(&mut self) { 140 self.edges.clear(); 141 self.nodes.clear(); 142 } 143 } 144 145 impl Default for DependencyGraph { 146 fn default() -> Self { 147 Self::new() 148 } 149 } 150 151 pub struct ResolutionCache { 152 hot_cache: Arc<RwLock<LruCache<CacheKey, CachedValue>>>, 153 ttl_cache: Arc<DashMap<CacheKey, CachedValue>>, 154 ttl: Duration, 155 enabled: bool, 156 } 157 158 impl ResolutionCache { 159 pub fn new(config: &super::config::CacheConfig) -> Self { 160 let hot_size = NonZeroUsize::new(config.hot_cache_size.max(1)) 161 .unwrap_or(NonZeroUsize::new(1000).unwrap()); 162 163 Self { 164 hot_cache: Arc::new(RwLock::new(LruCache::new(hot_size))), 165 ttl_cache: Arc::new(DashMap::new()), 166 ttl: config.ttl, 167 enabled: config.enabled, 168 } 169 } 170 171 pub fn get(&self, key: &CacheKey) -> Option<Arc<ResolvedVariable>> { 172 if !self.enabled { 173 return None; 174 } 175 176 let now = Instant::now(); 177 let ttl = self.ttl; 178 179 if let Some(cached) = self.ttl_cache.get(key) { 180 if now.duration_since(cached.cached_at) < ttl { 181 return Some(Arc::clone(&cached.value)); 182 } 183 } 184 185 self.ttl_cache 186 .remove_if(key, |_, cached| now.duration_since(cached.cached_at) >= ttl); 187 188 let mut hot = self.hot_cache.write(); 189 if let Some(cached) = hot.get(key) { 190 if now.duration_since(cached.cached_at) < ttl { 191 return Some(Arc::clone(&cached.value)); 192 } 193 } 194 195 None 196 } 197 198 pub fn insert(&self, key: CacheKey, value: Arc<ResolvedVariable>) { 199 if !self.enabled { 200 return; 201 } 202 203 let cached = CachedValue { 204 value, 205 cached_at: Instant::now(), 206 }; 207 208 self.ttl_cache.insert(key.clone(), cached.clone()); 209 210 let mut hot = self.hot_cache.write(); 211 hot.put(key, cached); 212 } 213 214 pub fn invalidate(&self, key: &CacheKey) { 215 if !self.enabled { 216 return; 217 } 218 219 self.ttl_cache.remove(key); 220 let mut hot = self.hot_cache.write(); 221 hot.pop(key); 222 } 223 224 pub fn clear(&self) { 225 self.ttl_cache.clear(); 226 let mut hot = self.hot_cache.write(); 227 hot.clear(); 228 } 229 230 pub fn len(&self) -> usize { 231 if !self.enabled { 232 return 0; 233 } 234 self.ttl_cache.len() + self.hot_cache.read().len() 235 } 236 237 pub fn is_empty(&self) -> bool { 238 !self.enabled || self.len() == 0 239 } 240 241 pub fn cleanup_expired(&self) { 242 if !self.enabled { 243 return; 244 } 245 246 let now = Instant::now(); 247 self.ttl_cache 248 .retain(|_, cached| now.duration_since(cached.cached_at) < self.ttl); 249 250 let mut hot = self.hot_cache.write(); 251 let keys_to_remove: Vec<CacheKey> = hot 252 .iter() 253 .filter(|(_, cached)| now.duration_since(cached.cached_at) >= self.ttl) 254 .map(|(k, _)| k.clone()) 255 .collect(); 256 257 for key in keys_to_remove { 258 hot.pop(&key); 259 } 260 } 261 } 262 263 pub struct ResolutionEngine { 264 resolution_config: parking_lot::RwLock<super::config::ResolutionConfig>, 265 interpolation_config: parking_lot::RwLock<super::config::InterpolationConfig>, 266 cache: Arc<ResolutionCache>, 267 graph: Arc<parking_lot::RwLock<DependencyGraph>>, 268 graph_version: Arc<AtomicU64>, 269 } 270 271 impl ResolutionEngine { 272 pub fn new( 273 resolution: &super::config::ResolutionConfig, 274 interpolation: &super::config::InterpolationConfig, 275 cache: &super::config::CacheConfig, 276 ) -> Self { 277 Self { 278 resolution_config: parking_lot::RwLock::new(resolution.clone()), 279 interpolation_config: parking_lot::RwLock::new(interpolation.clone()), 280 cache: Arc::new(ResolutionCache::new(cache)), 281 graph: Arc::new(parking_lot::RwLock::new(DependencyGraph::new())), 282 graph_version: Arc::new(AtomicU64::new(0)), 283 } 284 } 285 286 pub fn update_resolution_config(&self, config: super::config::ResolutionConfig) { 287 *self.resolution_config.write() = config; 288 self.cache.clear(); 289 tracing::info!("Resolution config updated at runtime"); 290 } 291 292 pub fn update_interpolation_config(&self, config: super::config::InterpolationConfig) { 293 *self.interpolation_config.write() = config; 294 self.cache.clear(); 295 tracing::info!("Interpolation config updated at runtime"); 296 } 297 298 pub fn interpolation_enabled(&self) -> bool { 299 self.interpolation_config.read().enabled 300 } 301 302 pub fn precedence(&self) -> Vec<super::config::SourcePrecedence> { 303 self.resolution_config.read().precedence.clone() 304 } 305 306 fn snapshots_version(&self, snapshots: &[crate::source::SourceSnapshot]) -> u64 { 307 snapshots.iter().filter_map(|s| s.version).sum() 308 } 309 310 fn maybe_rebuild_graph(&self, snapshots: &[crate::source::SourceSnapshot]) -> Result<()> { 311 let current_version = self.snapshots_version(snapshots); 312 let last_version = self.graph_version.load(Ordering::SeqCst); 313 314 if current_version != last_version { 315 self.build_dependency_graph(snapshots)?; 316 self.graph_version.store(current_version, Ordering::SeqCst); 317 } 318 Ok(()) 319 } 320 321 fn filter_snapshots_ref<'a>( 322 &self, 323 snapshots: &'a [crate::source::SourceSnapshot], 324 file_source_filter: Option<&HashSet<crate::source::SourceId>>, 325 ) -> Vec<&'a crate::source::SourceSnapshot> { 326 match file_source_filter { 327 Some(filter) if !filter.is_empty() => snapshots 328 .iter() 329 .filter(|snapshot| { 330 let source_id_str = snapshot.source_id.as_str(); 331 let is_file_source = 332 source_id_str.starts_with("file:") && source_id_str.len() > 5; 333 if is_file_source { 334 filter.contains(&snapshot.source_id) 335 } else { 336 true 337 } 338 }) 339 .collect(), 340 341 Some(_) => snapshots 342 .iter() 343 .filter(|snapshot| { 344 let source_id_str = snapshot.source_id.as_str(); 345 let is_file_source = 346 source_id_str.starts_with("file:") && source_id_str.len() > 5; 347 !is_file_source 348 }) 349 .collect(), 350 351 None => snapshots.iter().collect(), 352 } 353 } 354 355 fn filter_by_source_type<'a>( 356 &self, 357 snapshots: &[&'a crate::source::SourceSnapshot], 358 ) -> Vec<&'a crate::source::SourceSnapshot> { 359 let config = self.resolution_config.read(); 360 let precedence = &config.precedence; 361 362 if precedence.is_empty() { 363 return Vec::new(); 364 } 365 366 snapshots 367 .iter() 368 .filter(|snapshot| { 369 let source_id_str = snapshot.source_id.as_str(); 370 371 let source_type = if source_id_str.starts_with("file:") { 372 crate::config::SourcePrecedence::File 373 } else if source_id_str == "shell" || source_id_str.starts_with("shell:") { 374 crate::config::SourcePrecedence::Shell 375 } else if source_id_str.starts_with("external:") { 376 crate::config::SourcePrecedence::Remote 377 } else { 378 return true; 379 }; 380 381 precedence.contains(&source_type) 382 }) 383 .copied() 384 .collect() 385 } 386 387 fn resolve_inner( 388 &self, 389 key: &str, 390 context: &super::workspace::WorkspaceContext, 391 snapshots: &[crate::source::SourceSnapshot], 392 ) -> Result<Option<Arc<ResolvedVariable>>> { 393 let sorted_snapshots = self.sort_snapshots_by_file_order(snapshots); 394 395 let mut resolved = None; 396 397 for snapshot in &sorted_snapshots { 398 if let Some(variable) = snapshot.variables.iter().find(|v| v.key.as_str() == key) { 399 resolved = Some(self.resolve_variable( 400 variable, 401 snapshots, 402 context, 403 0, 404 &mut Vec::new(), 405 )?); 406 } 407 } 408 409 if let Some(ref var) = resolved { 410 let context_hash = self.hash_context(context); 411 let cache_key = CacheKey { 412 key: CompactString::new(key), 413 context_hash, 414 }; 415 self.cache.insert(cache_key, Arc::clone(var)); 416 } 417 418 Ok(resolved) 419 } 420 421 fn sort_snapshots_by_file_order<'a>( 422 &self, 423 snapshots: &'a [crate::source::SourceSnapshot], 424 ) -> Vec<&'a crate::source::SourceSnapshot> { 425 let config = self.resolution_config.read(); 426 let file_order = &config.files.order; 427 428 let mut sorted: Vec<_> = snapshots.iter().collect(); 429 sorted.sort_by(|a, b| { 430 let a_order = self.get_file_order_index(&a.source_id, file_order); 431 let b_order = self.get_file_order_index(&b.source_id, file_order); 432 a_order.cmp(&b_order) 433 }); 434 435 sorted 436 } 437 438 fn get_file_order_index( 439 &self, 440 source_id: &crate::source::SourceId, 441 file_order: &[CompactString], 442 ) -> usize { 443 let source_str = source_id.as_str(); 444 if !source_str.starts_with("file:") { 445 return 0; 446 } 447 448 let path = &source_str[5..]; 449 let filename = std::path::Path::new(path) 450 .file_name() 451 .and_then(|n| n.to_str()) 452 .unwrap_or(""); 453 454 for (i, pattern) in file_order.iter().enumerate() { 455 if filename == pattern.as_str() || path.ends_with(pattern.as_str()) { 456 return i + 1; 457 } 458 } 459 460 file_order.len() + 1 461 } 462 463 fn sort_snapshot_refs_by_file_order<'a>( 464 &self, 465 snapshots: &[&'a crate::source::SourceSnapshot], 466 ) -> Vec<&'a crate::source::SourceSnapshot> { 467 let config = self.resolution_config.read(); 468 let file_order = &config.files.order; 469 470 let mut sorted: Vec<_> = snapshots.to_vec(); 471 sorted.sort_by(|a, b| { 472 let a_order = self.get_file_order_index(&a.source_id, file_order); 473 let b_order = self.get_file_order_index(&b.source_id, file_order); 474 a_order.cmp(&b_order) 475 }); 476 477 sorted 478 } 479 480 fn all_variables_inner( 481 &self, 482 context: &super::workspace::WorkspaceContext, 483 all_snapshots: &[crate::source::SourceSnapshot], 484 filtered_snapshots: &[&crate::source::SourceSnapshot], 485 ) -> Result<Vec<Arc<ResolvedVariable>>> { 486 let type_filtered = self.filter_by_source_type(filtered_snapshots); 487 488 let sorted = self.sort_snapshot_refs_by_file_order(&type_filtered); 489 490 let mut seen_keys = std::collections::HashSet::new(); 491 let mut results = Vec::new(); 492 493 for snapshot in sorted { 494 for variable in snapshot.variables.iter() { 495 if !seen_keys.contains(&variable.key) { 496 let resolved = self.resolve_variable( 497 variable, 498 all_snapshots, 499 context, 500 0, 501 &mut Vec::new(), 502 )?; 503 results.push(resolved); 504 seen_keys.insert(variable.key.clone()); 505 } 506 } 507 } 508 509 Ok(results) 510 } 511 512 #[cfg_attr(feature = "async", must_be_async)] 513 #[cfg_attr(not(feature = "async"), must_be_sync)] 514 pub async fn resolve( 515 &self, 516 key: &str, 517 context: &super::workspace::WorkspaceContext, 518 registry: &super::source::SourceRegistry, 519 ) -> Result<Option<Arc<ResolvedVariable>>> { 520 let context_hash = self.hash_context(context); 521 let cache_key = CacheKey { 522 key: CompactString::new(key), 523 context_hash, 524 }; 525 526 if let Some(cached) = self.cache.get(&cache_key) { 527 return Ok(Some(cached)); 528 } 529 530 let snapshots = registry.load_all().await.map_err(AbundantisError::Source)?; 531 532 if self.resolution_config.read().type_check { 533 self.maybe_rebuild_graph(&snapshots)?; 534 } 535 536 self.resolve_inner(key, context, &snapshots) 537 } 538 539 fn resolve_variable( 540 &self, 541 variable: &super::source::ParsedVariable, 542 all_snapshots: &[crate::source::SourceSnapshot], 543 context: &super::workspace::WorkspaceContext, 544 depth: u32, 545 visited: &mut Vec<CompactString>, 546 ) -> Result<Arc<ResolvedVariable>> { 547 let key = variable.key.clone(); 548 let interpolation_config = self.interpolation_config.read(); 549 let max_depth = interpolation_config.max_depth; 550 551 if !interpolation_config.enabled { 552 return Ok(Arc::new(ResolvedVariable { 553 key: key.clone(), 554 raw_value: variable.raw_value.clone(), 555 resolved_value: variable.raw_value.clone(), 556 source: variable.source.clone(), 557 description: variable.description.clone(), 558 has_warnings: false, 559 interpolation_depth: 0, 560 })); 561 } 562 563 if depth >= max_depth { 564 return Err(AbundantisError::MaxDepthExceeded { 565 key: key.as_str().to_string(), 566 depth, 567 }); 568 } 569 570 if visited.contains(&key) { 571 return Err(AbundantisError::CircularDependency { 572 chain: visited 573 .iter() 574 .map(|k| k.as_str()) 575 .collect::<Vec<_>>() 576 .join(" -> "), 577 }); 578 } 579 580 visited.push(key.clone()); 581 582 let resolved_value = self.interpolate_value_lazy( 583 &variable.raw_value, 584 all_snapshots, 585 context, 586 depth + 1, 587 visited, 588 ); 589 590 visited.pop(); 591 592 Ok(Arc::new(ResolvedVariable { 593 key, 594 raw_value: variable.raw_value.clone(), 595 resolved_value, 596 source: variable.source.clone(), 597 description: variable.description.clone(), 598 has_warnings: false, 599 interpolation_depth: depth, 600 })) 601 } 602 603 fn interpolate_value_lazy( 604 &self, 605 value: &str, 606 all_snapshots: &[crate::source::SourceSnapshot], 607 _context: &super::workspace::WorkspaceContext, 608 depth: u32, 609 visited: &mut Vec<CompactString>, 610 ) -> CompactString { 611 let interpolation_config = self.interpolation_config.read(); 612 let max_depth = interpolation_config.max_depth; 613 614 if depth >= max_depth || !interpolation_config.enabled { 615 return CompactString::new(value); 616 } 617 618 let mut germi = germi::Germi::with_config(germi::Config { 619 max_depth: (max_depth - depth) as usize, 620 ..Default::default() 621 }); 622 623 let references = self.find_variable_references(value); 624 for ref_key in references { 625 if visited.contains(&ref_key) { 626 continue; 627 } 628 629 for snapshot in all_snapshots { 630 if let Some(variable) = snapshot.variables.iter().find(|v| v.key == ref_key) { 631 let resolved_value = self.interpolate_value_lazy( 632 &variable.raw_value, 633 all_snapshots, 634 _context, 635 depth + 1, 636 visited, 637 ); 638 germi.add_variable(variable.key.as_str(), resolved_value.as_str()); 639 break; 640 } 641 } 642 } 643 644 match germi.interpolate(value) { 645 Ok(interpolated) => CompactString::new(interpolated.as_ref()), 646 Err(e) => { 647 tracing::warn!( 648 value = %value, 649 depth = %depth, 650 error = %e, 651 "Interpolation failed, returning original value" 652 ); 653 CompactString::new(value) 654 } 655 } 656 } 657 658 #[cfg_attr(feature = "async", must_be_async)] 659 #[cfg_attr(not(feature = "async"), must_be_sync)] 660 pub async fn all_variables( 661 &self, 662 context: &super::workspace::WorkspaceContext, 663 registry: &super::source::SourceRegistry, 664 ) -> Result<Vec<Arc<ResolvedVariable>>> { 665 let snapshots = registry.load_all().await.map_err(AbundantisError::Source)?; 666 667 if self.resolution_config.read().type_check { 668 self.maybe_rebuild_graph(&snapshots)?; 669 } 670 671 self.all_variables_inner(context, &snapshots, &snapshots.iter().collect::<Vec<_>>()) 672 } 673 674 #[cfg_attr(feature = "async", must_be_async)] 675 #[cfg_attr(not(feature = "async"), must_be_sync)] 676 pub async fn resolve_with_filter( 677 &self, 678 key: &str, 679 context: &super::workspace::WorkspaceContext, 680 registry: &super::source::SourceRegistry, 681 file_source_filter: Option<&HashSet<super::source::SourceId>>, 682 ) -> Result<Option<Arc<ResolvedVariable>>> { 683 let context_hash = self.hash_context(context); 684 let cache_key = CacheKey { 685 key: CompactString::new(key), 686 context_hash, 687 }; 688 689 if let Some(cached) = self.cache.get(&cache_key) { 690 return Ok(Some(cached)); 691 } 692 693 let snapshots = registry.load_all().await.map_err(AbundantisError::Source)?; 694 let filtered_refs = self.filter_snapshots_ref(&snapshots, file_source_filter); 695 696 let type_filtered = self.filter_by_source_type(&filtered_refs); 697 698 if self.resolution_config.read().type_check { 699 self.maybe_rebuild_graph(&snapshots)?; 700 } 701 702 let sorted_filtered = self.sort_snapshot_refs_by_file_order(&type_filtered); 703 704 let mut resolved = None; 705 706 for snapshot in sorted_filtered { 707 if let Some(variable) = snapshot.variables.iter().find(|v| v.key.as_str() == key) { 708 resolved = Some(self.resolve_variable( 709 variable, 710 &snapshots, 711 context, 712 0, 713 &mut Vec::new(), 714 )?); 715 } 716 } 717 718 if let Some(ref var) = resolved { 719 self.cache.insert(cache_key, Arc::clone(var)); 720 } 721 722 Ok(resolved) 723 } 724 725 #[cfg_attr(feature = "async", must_be_async)] 726 #[cfg_attr(not(feature = "async"), must_be_sync)] 727 pub async fn all_variables_with_filter( 728 &self, 729 context: &super::workspace::WorkspaceContext, 730 registry: &super::source::SourceRegistry, 731 file_source_filter: Option<&HashSet<super::source::SourceId>>, 732 ) -> Result<Vec<Arc<ResolvedVariable>>> { 733 let snapshots = registry.load_all().await.map_err(AbundantisError::Source)?; 734 735 let filtered_refs = self.filter_snapshots_ref(&snapshots, file_source_filter); 736 737 if self.resolution_config.read().type_check { 738 self.maybe_rebuild_graph(&snapshots)?; 739 } 740 741 self.all_variables_inner(context, &snapshots, &filtered_refs) 742 } 743 744 fn hash_context(&self, context: &super::workspace::WorkspaceContext) -> u64 { 745 use ahash::AHasher; 746 use std::hash::{Hash, Hasher}; 747 748 let mut hasher = AHasher::default(); 749 context.workspace_root.hash(&mut hasher); 750 context.package_root.hash(&mut hasher); 751 context.package_name.hash(&mut hasher); 752 for env_file in &context.env_files { 753 env_file.hash(&mut hasher); 754 } 755 hasher.finish() 756 } 757 758 fn build_dependency_graph(&self, snapshots: &[crate::source::SourceSnapshot]) -> Result<()> { 759 let mut graph = self.graph.write(); 760 graph.clear(); 761 762 for snapshot in snapshots { 763 for variable in snapshot.variables.iter() { 764 let references = self.find_variable_references(&variable.raw_value); 765 for ref_key in references { 766 graph.add_edge(variable.key.clone(), ref_key, Some((0, 0))); 767 } 768 } 769 } 770 771 let mut visited = HashMap::new(); 772 let mut path = Vec::new(); 773 for snapshot in snapshots { 774 for variable in snapshot.variables.iter() { 775 let cycle = 776 graph.detect_cycle_with_state(variable.key.as_str(), &mut visited, &mut path); 777 if !cycle.is_empty() { 778 let chain = cycle 779 .iter() 780 .map(|k| k.as_str()) 781 .collect::<Vec<_>>() 782 .join(" -> "); 783 return Err(AbundantisError::CircularDependency { 784 chain: format!("{} -> {}", chain, variable.key), 785 }); 786 } 787 } 788 } 789 790 Ok(()) 791 } 792 793 fn find_variable_references(&self, value: &str) -> Vec<CompactString> { 794 germi::find_variable_references(value) 795 .into_iter() 796 .map(CompactString::new) 797 .collect() 798 } 799 800 pub fn cache(&self) -> &Arc<ResolutionCache> { 801 &self.cache 802 } 803 804 pub fn graph(&self) -> &Arc<parking_lot::RwLock<DependencyGraph>> { 805 &self.graph 806 } 807 } 808 809 #[cfg(test)] 810 mod tests { 811 use super::*; 812 813 #[test] 814 fn test_cache_basics() { 815 let config = super::super::config::CacheConfig { 816 enabled: true, 817 hot_cache_size: 100, 818 ttl: Duration::from_secs(60), 819 }; 820 821 let cache = ResolutionCache::new(&config); 822 assert!(cache.is_empty()); 823 824 let key = CacheKey { 825 key: CompactString::new("TEST"), 826 context_hash: 123, 827 }; 828 829 let var = Arc::new(ResolvedVariable { 830 key: CompactString::new("TEST"), 831 raw_value: CompactString::new("value"), 832 resolved_value: CompactString::new("value"), 833 source: super::super::source::VariableSource::Memory, 834 description: None, 835 has_warnings: false, 836 interpolation_depth: 0, 837 }); 838 839 cache.insert(key.clone(), var.clone()); 840 assert!(!cache.is_empty()); 841 assert_eq!(cache.len(), 2); 842 843 let retrieved = cache.get(&key).unwrap(); 844 assert_eq!(retrieved.key.as_str(), "TEST"); 845 } 846 847 #[test] 848 fn test_dependency_cycle_detection() { 849 let mut graph = DependencyGraph::new(); 850 851 graph.add_edge(CompactString::new("A"), CompactString::new("B"), None); 852 graph.add_edge(CompactString::new("B"), CompactString::new("C"), None); 853 graph.add_edge(CompactString::new("C"), CompactString::new("A"), None); 854 855 let cycle = graph.detect_cycle("A"); 856 assert!(!cycle.is_empty()); 857 assert!(cycle.contains(&CompactString::new("A"))); 858 } 859 }