/ src / source / remote / external.rs
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  }