external.rs
1 //! External provider adapter. 2 //! 3 //! This module implements `ExternalProviderAdapter`, which spawns and manages 4 //! out-of-process provider binaries. It handles: 5 //! 6 //! - Process lifecycle (spawn, health check, restart, shutdown) 7 //! - JSON-RPC protocol communication 8 //! - AsyncEnvSource implementation 9 //! - Authentication state management 10 //! - Scope selection and secret caching 11 //! 12 //! ## Crash Recovery 13 //! 14 //! If the provider process crashes, the adapter uses exponential backoff: 15 //! | Attempt | Delay | 16 //! |---------|-------| 17 //! | 1 | 0s | 18 //! | 2 | 1s | 19 //! | 3 | 2s | 20 //! | 4 | 4s | 21 //! | 5 | 8s (then stop) | 22 23 use super::discovery::ProviderDiscovery; 24 use super::protocol::{ 25 self, methods, AuthField, AuthStatus, AuthenticateParams, AuthenticateResult, 26 ClientCapabilities, ClientInfo, InitializeParams, InitializeResult, PingParams, 27 ProviderCapabilities, ProviderInfo, ScopeLevel, ScopeLevelsResult, ScopeOption, 28 ScopeOptionsParams, ScopeOptionsResult, ScopeSelection, Secret, SecretsFetchParams, 29 SecretsFetchResult, PROTOCOL_VERSION, 30 }; 31 use super::traits::RemoteSourceInfo; 32 use super::transport::{spawn_provider, StdioTransport}; 33 use crate::config::ExternalProviderConfig; 34 use crate::error::SourceError; 35 use crate::source::traits::{ 36 AsyncEnvSource, Priority, SourceCapabilities, SourceId, SourceMetadata, 37 SourceSnapshot, SourceType, 38 }; 39 use crate::source::variable::{ParsedVariable, VariableSource}; 40 use async_trait::async_trait; 41 use compact_str::CompactString; 42 use parking_lot::RwLock; 43 use std::collections::HashMap; 44 use std::path::PathBuf; 45 use std::process::Child; 46 use std::sync::Arc; 47 use std::time::{Duration, Instant}; 48 49 /// Maximum number of restart attempts before giving up. 50 const MAX_RESTART_ATTEMPTS: u32 = 5; 51 52 /// Health check interval (used by ProviderManager for scheduling). 53 #[allow(dead_code)] 54 const HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(30); 55 56 /// Health check timeout. 57 const HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(5); 58 59 /// Request timeout. 60 const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); 61 62 /// External provider state. 63 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 64 pub enum ProviderState { 65 /// Not spawned yet. 66 NotStarted, 67 /// Starting up. 68 Starting, 69 /// Running and healthy. 70 Running, 71 /// Not authenticated. 72 NeedsAuth, 73 /// Process crashed, will attempt restart. 74 Crashed, 75 /// Stopped (manually or after max restarts). 76 Stopped, 77 } 78 79 /// External provider adapter. 80 /// 81 /// Manages an out-of-process provider binary, handling lifecycle and 82 /// implementing `AsyncEnvSource` by delegating to the external process. 83 pub struct ExternalProviderAdapter { 84 /// Provider ID (e.g., "doppler"). 85 provider_id: String, 86 /// Path to the provider binary. 87 binary_path: PathBuf, 88 /// Provider configuration. 89 config: ExternalProviderConfig, 90 /// Source ID for this adapter. 91 source_id: SourceId, 92 /// Child process handle. 93 process: RwLock<Option<Child>>, 94 /// Transport for communication (wrapped in Arc for Send across await). 95 transport: RwLock<Option<Arc<StdioTransport>>>, 96 /// Provider state. 97 state: RwLock<ProviderState>, 98 /// Provider info from initialize response. 99 provider_info: RwLock<Option<ProviderInfo>>, 100 /// Provider capabilities. 101 capabilities: RwLock<ProviderCapabilities>, 102 /// Current auth status. 103 auth_status: RwLock<AuthStatus>, 104 /// Current scope selection. 105 scope: RwLock<ScopeSelection>, 106 /// Cached secrets. 107 cached_secrets: RwLock<Option<SecretsFetchResult>>, 108 /// Last successful refresh time. 109 last_refreshed: RwLock<Option<Instant>>, 110 /// Restart attempt count. 111 restart_count: RwLock<u32>, 112 /// Last error message. 113 last_error: RwLock<Option<String>>, 114 /// Last health check time. 115 last_health_check: RwLock<Option<Instant>>, 116 } 117 118 impl ExternalProviderAdapter { 119 /// Creates a new external provider adapter. 120 pub fn new( 121 provider_id: impl Into<String>, 122 binary_path: impl Into<PathBuf>, 123 config: ExternalProviderConfig, 124 ) -> Self { 125 let provider_id = provider_id.into(); 126 let source_id = SourceId::new(format!("external:{}", provider_id)); 127 128 Self { 129 provider_id, 130 binary_path: binary_path.into(), 131 config, 132 source_id, 133 process: RwLock::new(None), 134 transport: RwLock::new(None), 135 state: RwLock::new(ProviderState::NotStarted), 136 provider_info: RwLock::new(None), 137 capabilities: RwLock::new(ProviderCapabilities::default()), 138 auth_status: RwLock::new(AuthStatus::NotAuthenticated), 139 scope: RwLock::new(ScopeSelection::default()), 140 cached_secrets: RwLock::new(None), 141 last_refreshed: RwLock::new(None), 142 restart_count: RwLock::new(0), 143 last_error: RwLock::new(None), 144 last_health_check: RwLock::new(None), 145 } 146 } 147 148 /// Creates an adapter by discovering the provider binary. 149 pub fn discover( 150 provider_id: &str, 151 config: ExternalProviderConfig, 152 discovery: &ProviderDiscovery, 153 ) -> Result<Self, SourceError> { 154 // Check for binary override first 155 let binary_path = if let Some(ref path) = config.binary { 156 path.clone() 157 } else { 158 discovery.find_provider(provider_id)? 159 }; 160 161 Ok(Self::new(provider_id, binary_path, config)) 162 } 163 164 /// Returns the provider ID. 165 pub fn provider_id(&self) -> &str { 166 &self.provider_id 167 } 168 169 /// Returns the current state. 170 pub fn state(&self) -> ProviderState { 171 *self.state.read() 172 } 173 174 /// Returns the provider info if available. 175 pub fn provider_info(&self) -> Option<ProviderInfo> { 176 self.provider_info.read().clone() 177 } 178 179 /// Returns the current auth status. 180 pub fn auth_status(&self) -> AuthStatus { 181 self.auth_status.read().clone() 182 } 183 184 /// Returns the current scope selection. 185 pub fn scope(&self) -> ScopeSelection { 186 self.scope.read().clone() 187 } 188 189 /// Returns the last error message. 190 pub fn last_error(&self) -> Option<String> { 191 self.last_error.read().clone() 192 } 193 194 /// Gets the transport Arc for use in async methods. 195 /// This clones the Arc so the lock can be released before await. 196 fn get_transport(&self) -> Result<Arc<StdioTransport>, SourceError> { 197 self.transport 198 .read() 199 .as_ref() 200 .cloned() 201 .ok_or_else(|| SourceError::Connection { 202 provider: self.provider_id.clone(), 203 reason: "Not connected".into(), 204 }) 205 } 206 207 /// Spawns the provider process and initializes it. 208 pub async fn spawn(&self) -> Result<(), SourceError> { 209 // Check if already running 210 if *self.state.read() == ProviderState::Running { 211 return Ok(()); 212 } 213 214 *self.state.write() = ProviderState::Starting; 215 216 // Spawn the process 217 let mut child = spawn_provider(&self.binary_path).map_err(|e| SourceError::Remote { 218 provider: self.provider_id.clone(), 219 reason: e.to_string(), 220 })?; 221 222 // Create transport 223 let transport = StdioTransport::new(&mut child).map_err(|e| SourceError::Remote { 224 provider: self.provider_id.clone(), 225 reason: e.to_string(), 226 })?; 227 228 *self.process.write() = Some(child); 229 *self.transport.write() = Some(Arc::new(transport)); 230 231 // Initialize the provider 232 self.initialize().await?; 233 234 // Check auth status 235 self.refresh_auth_status().await?; 236 237 if self.auth_status.read().is_authenticated() { 238 *self.state.write() = ProviderState::Running; 239 } else { 240 *self.state.write() = ProviderState::NeedsAuth; 241 } 242 243 *self.restart_count.write() = 0; 244 *self.last_error.write() = None; 245 246 Ok(()) 247 } 248 249 /// Initializes the provider with capability negotiation. 250 async fn initialize(&self) -> Result<InitializeResult, SourceError> { 251 let transport = self.get_transport()?; 252 253 let params = InitializeParams { 254 protocol_version: PROTOCOL_VERSION.to_string(), 255 client_info: ClientInfo { 256 name: "ecolog-lsp".to_string(), 257 version: env!("CARGO_PKG_VERSION").to_string(), 258 }, 259 capabilities: ClientCapabilities { 260 notifications: protocol::NotificationCapabilities { 261 secrets_changed: true, 262 }, 263 credential_storage: true, 264 }, 265 config: self.config.settings.clone(), 266 }; 267 268 let response = transport 269 .request_with_timeout(methods::INITIALIZE, params, REQUEST_TIMEOUT) 270 .await 271 .map_err(|e| SourceError::Connection { 272 provider: self.provider_id.clone(), 273 reason: e.to_string(), 274 })?; 275 276 let result: InitializeResult = response.into_result().map_err(|e| SourceError::Remote { 277 provider: self.provider_id.clone(), 278 reason: e.to_string(), 279 })?; 280 281 // Store provider info and capabilities 282 *self.provider_info.write() = Some(result.provider_info.clone()); 283 *self.capabilities.write() = result.capabilities.clone(); 284 285 // Send initialized notification 286 transport 287 .notify(methods::INITIALIZED, serde_json::json!({})) 288 .map_err(|e| SourceError::Connection { 289 provider: self.provider_id.clone(), 290 reason: e.to_string(), 291 })?; 292 293 Ok(result) 294 } 295 296 /// Refreshes the auth status from the provider. 297 async fn refresh_auth_status(&self) -> Result<AuthStatus, SourceError> { 298 let transport = self.get_transport()?; 299 300 let response = transport 301 .request_with_timeout(methods::AUTH_STATUS, serde_json::json!({}), REQUEST_TIMEOUT) 302 .await 303 .map_err(|e| SourceError::Connection { 304 provider: self.provider_id.clone(), 305 reason: e.to_string(), 306 })?; 307 308 let result: protocol::AuthStatusResult = 309 response.into_result().map_err(|e| SourceError::Remote { 310 provider: self.provider_id.clone(), 311 reason: e.to_string(), 312 })?; 313 314 *self.auth_status.write() = result.status.clone(); 315 Ok(result.status) 316 } 317 318 /// Returns the authentication fields required by this provider. 319 pub async fn auth_fields(&self) -> Result<Vec<AuthField>, SourceError> { 320 self.ensure_running().await?; 321 322 let transport = self.get_transport()?; 323 324 let response = transport 325 .request_with_timeout(methods::AUTH_FIELDS, serde_json::json!({}), REQUEST_TIMEOUT) 326 .await 327 .map_err(|e| SourceError::Connection { 328 provider: self.provider_id.clone(), 329 reason: e.to_string(), 330 })?; 331 332 let result: protocol::AuthFieldsResult = 333 response.into_result().map_err(|e| SourceError::Remote { 334 provider: self.provider_id.clone(), 335 reason: e.to_string(), 336 })?; 337 338 Ok(result.fields) 339 } 340 341 /// Authenticates with the provider. 342 pub async fn authenticate( 343 &self, 344 credentials: HashMap<String, String>, 345 ) -> Result<AuthStatus, SourceError> { 346 self.ensure_running().await?; 347 348 let transport = self.get_transport()?; 349 350 let params = AuthenticateParams { credentials }; 351 352 let response = transport 353 .request_with_timeout(methods::AUTH_AUTHENTICATE, params, REQUEST_TIMEOUT) 354 .await 355 .map_err(|e| SourceError::Connection { 356 provider: self.provider_id.clone(), 357 reason: e.to_string(), 358 })?; 359 360 let result: AuthenticateResult = 361 response.into_result().map_err(|_e| SourceError::Authentication { 362 source_name: self.provider_id.clone(), 363 })?; 364 365 *self.auth_status.write() = result.status.clone(); 366 367 if result.status.is_authenticated() { 368 *self.state.write() = ProviderState::Running; 369 } 370 371 Ok(result.status) 372 } 373 374 /// Returns the scope levels for this provider. 375 pub async fn scope_levels(&self) -> Result<Vec<ScopeLevel>, SourceError> { 376 self.ensure_running().await?; 377 378 let transport = self.get_transport()?; 379 380 let response = transport 381 .request_with_timeout(methods::SCOPE_LEVELS, serde_json::json!({}), REQUEST_TIMEOUT) 382 .await 383 .map_err(|e| SourceError::Connection { 384 provider: self.provider_id.clone(), 385 reason: e.to_string(), 386 })?; 387 388 let result: ScopeLevelsResult = 389 response.into_result().map_err(|e| SourceError::Remote { 390 provider: self.provider_id.clone(), 391 reason: e.to_string(), 392 })?; 393 394 Ok(result.levels) 395 } 396 397 /// Lists available options at the given scope level. 398 pub async fn list_scope_options( 399 &self, 400 level: &str, 401 parent: &ScopeSelection, 402 ) -> Result<Vec<ScopeOption>, SourceError> { 403 self.ensure_running().await?; 404 405 let transport = self.get_transport()?; 406 407 let params = ScopeOptionsParams { 408 level: level.to_string(), 409 parent: parent.selections.clone(), 410 }; 411 412 let response = transport 413 .request_with_timeout(methods::SCOPE_OPTIONS, params, REQUEST_TIMEOUT) 414 .await 415 .map_err(|e| SourceError::Connection { 416 provider: self.provider_id.clone(), 417 reason: e.to_string(), 418 })?; 419 420 let result: ScopeOptionsResult = 421 response.into_result().map_err(|e| SourceError::Remote { 422 provider: self.provider_id.clone(), 423 reason: e.to_string(), 424 })?; 425 426 Ok(result.options) 427 } 428 429 /// Sets the scope selection. 430 pub fn set_scope(&self, scope: ScopeSelection) { 431 *self.scope.write() = scope; 432 // Clear cache when scope changes 433 *self.cached_secrets.write() = None; 434 } 435 436 /// Fetches secrets for the current scope. 437 pub async fn fetch_secrets(&self) -> Result<Vec<Secret>, SourceError> { 438 self.ensure_running().await?; 439 self.ensure_authenticated()?; 440 441 let scope = self.scope.read().clone(); 442 let transport = self.get_transport()?; 443 444 let params = SecretsFetchParams { scope }; 445 446 let response = transport 447 .request_with_timeout(methods::SECRETS_FETCH, params, REQUEST_TIMEOUT) 448 .await 449 .map_err(|e| SourceError::Connection { 450 provider: self.provider_id.clone(), 451 reason: e.to_string(), 452 })?; 453 454 let result: SecretsFetchResult = 455 response.into_result().map_err(|e| SourceError::Remote { 456 provider: self.provider_id.clone(), 457 reason: e.to_string(), 458 })?; 459 460 // Cache the result 461 *self.cached_secrets.write() = Some(result.clone()); 462 *self.last_refreshed.write() = Some(Instant::now()); 463 464 Ok(result.secrets) 465 } 466 467 /// Performs a health check on the provider. 468 pub async fn health_check(&self) -> Result<bool, SourceError> { 469 if *self.state.read() != ProviderState::Running { 470 return Ok(false); 471 } 472 473 let transport = match self.get_transport() { 474 Ok(t) => t, 475 Err(_) => return Ok(false), 476 }; 477 478 let result = transport 479 .request_with_timeout(methods::PING, PingParams {}, HEALTH_CHECK_TIMEOUT) 480 .await; 481 482 *self.last_health_check.write() = Some(Instant::now()); 483 484 match result { 485 Ok(_) => Ok(true), 486 Err(_) => { 487 *self.state.write() = ProviderState::Crashed; 488 Ok(false) 489 } 490 } 491 } 492 493 /// Attempts to restart the provider if it crashed. 494 pub async fn restart_if_needed(&self) -> Result<bool, SourceError> { 495 let state = *self.state.read(); 496 if state != ProviderState::Crashed && state != ProviderState::Stopped { 497 return Ok(false); 498 } 499 500 let restart_count = *self.restart_count.read(); 501 if restart_count >= MAX_RESTART_ATTEMPTS { 502 tracing::warn!( 503 "Provider {} exceeded max restart attempts", 504 self.provider_id 505 ); 506 return Ok(false); 507 } 508 509 // Exponential backoff 510 let delay = Duration::from_secs(1 << restart_count.min(3)); 511 tokio::time::sleep(delay).await; 512 513 *self.restart_count.write() = restart_count + 1; 514 515 tracing::info!( 516 "Restarting provider {} (attempt {})", 517 self.provider_id, 518 restart_count + 1 519 ); 520 521 // Clean up old process 522 self.cleanup().await; 523 524 // Spawn new process 525 match self.spawn().await { 526 Ok(()) => Ok(true), 527 Err(e) => { 528 *self.last_error.write() = Some(e.to_string()); 529 *self.state.write() = ProviderState::Crashed; 530 Err(e) 531 } 532 } 533 } 534 535 /// Gracefully shuts down the provider. 536 pub async fn shutdown(&self) -> Result<(), SourceError> { 537 let state = *self.state.read(); 538 if state == ProviderState::NotStarted || state == ProviderState::Stopped { 539 return Ok(()); 540 } 541 542 *self.state.write() = ProviderState::Stopped; 543 544 // Send shutdown request - get transport first, then await 545 if let Ok(transport) = self.get_transport() { 546 let _ = transport 547 .request_with_timeout( 548 methods::SHUTDOWN, 549 serde_json::json!({}), 550 Duration::from_secs(5), 551 ) 552 .await; 553 554 // Send exit notification 555 let _ = transport.notify(methods::EXIT, serde_json::json!({})); 556 } 557 558 // Wait briefly then cleanup 559 tokio::time::sleep(Duration::from_millis(100)).await; 560 self.cleanup().await; 561 562 Ok(()) 563 } 564 565 /// Cleans up the process and transport. 566 /// 567 /// Note: We don't call transport.shutdown() here because: 568 /// 1. We've already sent SHUTDOWN/EXIT messages in shutdown() 569 /// 2. StdioTransport::shutdown requires &mut self which isn't compatible with Arc 570 /// 3. Dropping the Arc will cause the transport to clean up its resources 571 async fn cleanup(&self) { 572 // Drop the transport - this releases our reference and lets it clean up 573 let _ = self.transport.write().take(); 574 575 if let Some(mut process) = self.process.write().take() { 576 // Try graceful termination first 577 let _ = process.kill(); 578 let _ = process.wait(); 579 } 580 } 581 582 /// Ensures the provider is running, spawning if necessary. 583 async fn ensure_running(&self) -> Result<(), SourceError> { 584 let state = *self.state.read(); 585 match state { 586 ProviderState::Running | ProviderState::NeedsAuth => Ok(()), 587 ProviderState::NotStarted | ProviderState::Starting => self.spawn().await, 588 ProviderState::Crashed => { 589 self.restart_if_needed().await?; 590 Ok(()) 591 } 592 ProviderState::Stopped => Err(SourceError::Remote { 593 provider: self.provider_id.clone(), 594 reason: "Provider is stopped".into(), 595 }), 596 } 597 } 598 599 /// Ensures the provider is authenticated. 600 fn ensure_authenticated(&self) -> Result<(), SourceError> { 601 if !self.auth_status.read().is_authenticated() { 602 return Err(SourceError::Authentication { 603 source_name: self.provider_id.clone(), 604 }); 605 } 606 Ok(()) 607 } 608 609 /// Returns summary info for UI display. 610 pub fn info(&self) -> RemoteSourceInfo { 611 let provider_info = self.provider_info.read(); 612 let (id, display_name, short_name) = if let Some(ref info) = *provider_info { 613 ( 614 CompactString::from(&info.id), 615 CompactString::from(&info.name), 616 info.short_name 617 .as_ref() 618 .map(|s| CompactString::from(s.as_str())) 619 .unwrap_or_else(|| CompactString::from(&info.id)), 620 ) 621 } else { 622 ( 623 CompactString::from(&self.provider_id), 624 CompactString::from(&self.provider_id), 625 CompactString::from(&self.provider_id), 626 ) 627 }; 628 629 let auth_status = self.auth_status.read().clone().into(); 630 let scope = self.scope.read().clone().into(); 631 let secret_count = self 632 .cached_secrets 633 .read() 634 .as_ref() 635 .map(|s| s.secrets.len()) 636 .unwrap_or(0); 637 let last_refreshed = self.last_refreshed.read().map(|i| { 638 std::time::SystemTime::now() 639 .duration_since(std::time::UNIX_EPOCH) 640 .unwrap_or_default() 641 .as_millis() as u64 642 - i.elapsed().as_millis() as u64 643 }); 644 let loading = *self.state.read() == ProviderState::Starting; 645 let last_error = self.last_error.read().clone().map(CompactString::from); 646 647 RemoteSourceInfo { 648 id, 649 display_name, 650 short_name, 651 auth_status, 652 scope, 653 secret_count, 654 last_refreshed, 655 loading, 656 last_error, 657 } 658 } 659 660 /// Invalidates the cached secrets. 661 pub fn invalidate_cache(&self) { 662 *self.cached_secrets.write() = None; 663 } 664 } 665 666 #[async_trait] 667 impl AsyncEnvSource for ExternalProviderAdapter { 668 fn id(&self) -> &SourceId { 669 &self.source_id 670 } 671 672 fn source_type(&self) -> SourceType { 673 SourceType::Remote 674 } 675 676 fn priority(&self) -> Priority { 677 Priority::REMOTE 678 } 679 680 fn capabilities(&self) -> SourceCapabilities { 681 let caps = self.capabilities.read(); 682 let mut result = SourceCapabilities::ASYNC_ONLY | SourceCapabilities::SECRETS; 683 684 if caps.secrets.read { 685 result |= SourceCapabilities::READ; 686 } 687 if caps.secrets.write { 688 result |= SourceCapabilities::WRITE; 689 } 690 if caps.secrets.watch { 691 result |= SourceCapabilities::WATCH; 692 } 693 694 result 695 } 696 697 async fn load(&self) -> Result<SourceSnapshot, SourceError> { 698 // Clone cached data immediately to release the RwLock guard before any await. 699 // This is necessary because parking_lot guards contain non-Send raw pointers. 700 let cached_data = self.cached_secrets.read().clone(); 701 702 if let Some(cached) = cached_data { 703 let variables: Vec<ParsedVariable> = cached 704 .secrets 705 .iter() 706 .map(|s| ParsedVariable { 707 key: CompactString::from(&s.key), 708 raw_value: CompactString::from(&s.value), 709 source: VariableSource::Remote { 710 provider: CompactString::from(&self.provider_id), 711 path: cached.scope_path.clone(), 712 }, 713 description: s.description.as_ref().map(|d| CompactString::from(d.as_str())), 714 is_commented: false, 715 }) 716 .collect(); 717 718 return Ok(SourceSnapshot { 719 source_id: self.source_id.clone(), 720 variables: Arc::from(variables), 721 timestamp: Instant::now(), 722 version: None, 723 }); 724 } 725 726 // Fetch secrets 727 let secrets = self.fetch_secrets().await?; 728 729 // Get scope_path from the freshly cached data 730 let scope_path = self.cached_secrets.read().as_ref().and_then(|c| c.scope_path.clone()); 731 732 let variables: Vec<ParsedVariable> = secrets 733 .iter() 734 .map(|s| ParsedVariable { 735 key: CompactString::from(&s.key), 736 raw_value: CompactString::from(&s.value), 737 source: VariableSource::Remote { 738 provider: CompactString::from(&self.provider_id), 739 path: scope_path.clone(), 740 }, 741 description: s.description.as_ref().map(|d| CompactString::from(d.as_str())), 742 is_commented: false, 743 }) 744 .collect(); 745 746 Ok(SourceSnapshot { 747 source_id: self.source_id.clone(), 748 variables: Arc::from(variables), 749 timestamp: Instant::now(), 750 version: None, 751 }) 752 } 753 754 async fn refresh(&self) -> Result<bool, SourceError> { 755 self.invalidate_cache(); 756 self.load().await?; 757 Ok(true) 758 } 759 760 fn metadata(&self) -> SourceMetadata { 761 let info = self.provider_info.read(); 762 SourceMetadata { 763 display_name: info 764 .as_ref() 765 .map(|i| CompactString::from(&i.name)), 766 description: info 767 .as_ref() 768 .and_then(|i| i.description.as_ref().map(|d| CompactString::from(d.as_str()))), 769 last_refreshed: *self.last_refreshed.read(), 770 error_count: 0, 771 } 772 } 773 } 774 775 impl std::fmt::Debug for ExternalProviderAdapter { 776 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 777 f.debug_struct("ExternalProviderAdapter") 778 .field("provider_id", &self.provider_id) 779 .field("state", &*self.state.read()) 780 .field("auth_status", &*self.auth_status.read()) 781 .field("scope", &*self.scope.read()) 782 .finish() 783 } 784 } 785 786 #[cfg(test)] 787 mod tests { 788 use super::*; 789 790 #[test] 791 fn test_provider_state_default() { 792 let config = ExternalProviderConfig::default(); 793 let adapter = ExternalProviderAdapter::new("test", "/path/to/provider", config); 794 795 assert_eq!(adapter.state(), ProviderState::NotStarted); 796 assert_eq!(adapter.provider_id(), "test"); 797 } 798 799 #[test] 800 fn test_source_id_format() { 801 let config = ExternalProviderConfig::default(); 802 let adapter = ExternalProviderAdapter::new("doppler", "/path/to/provider", config); 803 804 assert_eq!(adapter.id().as_str(), "external:doppler"); 805 } 806 }