use anyhow::{Context, Result};
use tracing::{info, warn};
use super::RepositoryApi;
use crate::provider::{ProviderFactory, ProviderType};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FallbackStrategy {
None,
Auto,
Prompt,
}
#[derive(Debug, Clone)]
pub enum FallbackResult {
NotNeeded,
Success {
original_provider: ProviderType,
fallback_provider: ProviderType,
},
Failed {
original_provider: ProviderType,
error: String,
},
Declined { original_provider: ProviderType },
}
pub struct FallbackHandler {
strategy: FallbackStrategy,
nap_home: std::path::PathBuf,
}
impl FallbackHandler {
pub fn new(nap_home: &std::path::Path) -> Self {
Self {
strategy: FallbackStrategy::Prompt,
nap_home: nap_home.to_path_buf(),
}
}
pub fn with_strategy(mut self, strategy: FallbackStrategy) -> Self {
self.strategy = strategy;
self
}
pub async fn handle_provider_failure(
&self,
repository_api: &mut RepositoryApi,
original_provider: ProviderType,
error: &str,
) -> Result<FallbackResult> {
warn!(
"Provider failure detected: {} - {}",
original_provider.as_str(),
error
);
if !matches!(
original_provider,
ProviderType::PortalsCloud | ProviderType::Remote
) {
return Ok(FallbackResult::Failed {
original_provider,
error: format!(
"Cannot fallback from {} provider",
original_provider.as_str()
),
});
}
match self.strategy {
FallbackStrategy::None => Ok(FallbackResult::Failed {
original_provider,
error: "Fallback disabled".to_string(),
}),
FallbackStrategy::Auto => {
self.perform_fallback(repository_api, original_provider)
.await
}
FallbackStrategy::Prompt => {
info!("Prompting user for fallback to local provider");
self.perform_fallback(repository_api, original_provider)
.await
}
}
}
async fn perform_fallback(
&self,
repository_api: &mut RepositoryApi,
original_provider: ProviderType,
) -> Result<FallbackResult> {
info!("Attempting fallback to local provider");
let factory = ProviderFactory::new(&self.nap_home);
let local_provider = factory
.create_provider(ProviderType::Local)
.context("Failed to create local provider for fallback")?;
local_provider
.initialize()
.await
.context("Failed to initialize local provider during fallback")?;
local_provider
.ensure_ready()
.await
.context("Failed to ensure local provider ready during fallback")?;
repository_api
.provider_manager_mut()
.set_active_provider(local_provider.clone());
repository_api
.provider_manager_mut()
.save_provider_config(local_provider.as_ref())?;
info!("Fallback to local provider successful");
Ok(FallbackResult::Success {
original_provider,
fallback_provider: ProviderType::Local,
})
}
pub fn should_offer_fallback(&self, error: &str) -> bool {
error.to_lowercase().contains("unavailable")
|| error.to_lowercase().contains("timeout")
|| error.to_lowercase().contains("connection")
|| error.to_lowercase().contains("network")
}
pub fn fallback_message(&self, original_provider: ProviderType) -> String {
match original_provider {
ProviderType::PortalsCloud => "Portals Cloud is currently unavailable.\n\
Start a local Lore server instead?\n\
Changes will remain local until synchronization.\n\
[Y/n]"
.to_string(),
ProviderType::Remote => "Remote Lore server is currently unavailable.\n\
Start a local Lore server instead?\n\
Changes will remain local until synchronization.\n\
[Y/n]"
.to_string(),
ProviderType::Local => "Local provider failed. No fallback available.".to_string(),
}
}
}
pub trait RepositoryApiFallback {
fn with_fallback<F, Fut, T>(
&mut self,
operation: F,
) -> impl std::future::Future<Output = Result<T>> + Send
where
F: FnMut(&mut Self) -> Fut + Send,
Fut: std::future::Future<Output = Result<T>> + Send,
T: Send + 'static;
fn ensure_provider_with_fallback(
&mut self,
) -> impl std::future::Future<Output = Result<bool>> + Send;
}
impl RepositoryApiFallback for RepositoryApi {
async fn with_fallback<F, Fut, T>(&mut self, mut operation: F) -> Result<T>
where
F: FnMut(&mut Self) -> Fut + Send,
Fut: std::future::Future<Output = Result<T>> + Send,
T: Send + 'static,
{
let result = operation(self).await;
if let Err(e) = &result {
if let Some(provider) = self.active_provider() {
let handler = FallbackHandler::new(&self.nap_home);
let provider_type = provider.provider_type();
if handler.should_offer_fallback(&e.to_string()) {
let fallback_result = handler
.handle_provider_failure(self, provider_type, &e.to_string())
.await?;
match fallback_result {
FallbackResult::Success { .. } => {
operation(self).await
}
FallbackResult::Declined { .. } => result,
FallbackResult::Failed { error, .. } => Err(anyhow::anyhow!(error)),
FallbackResult::NotNeeded => result,
}
} else {
result
}
} else {
result
}
} else {
result
}
}
async fn ensure_provider_with_fallback(&mut self) -> Result<bool> {
let handler = FallbackHandler::new(&self.nap_home);
if let Some(provider) = self.active_provider() {
let provider_type = provider.provider_type();
let is_healthy = provider.health_check().await.unwrap_or(false);
if is_healthy {
Ok(true)
} else {
let fallback_result = handler
.handle_provider_failure(self, provider_type, "Provider health check failed")
.await
.map_err(|e| anyhow::anyhow!(e))?;
match fallback_result {
FallbackResult::Success { .. } => Ok(true),
FallbackResult::Declined { .. } => Ok(false),
FallbackResult::Failed { error, .. } => Err(anyhow::anyhow!(error)),
FallbackResult::NotNeeded => Ok(true),
}
}
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_fallback_handler_creation() {
let temp_dir = TempDir::new().unwrap();
let handler = FallbackHandler::new(temp_dir.path());
assert_eq!(handler.strategy, FallbackStrategy::Prompt);
}
#[test]
fn test_fallback_strategy() {
let temp_dir = TempDir::new().unwrap();
let handler = FallbackHandler::new(temp_dir.path()).with_strategy(FallbackStrategy::Auto);
assert_eq!(handler.strategy, FallbackStrategy::Auto);
}
#[test]
fn test_should_offer_fallback() {
let temp_dir = TempDir::new().unwrap();
let handler = FallbackHandler::new(temp_dir.path());
assert!(handler.should_offer_fallback("Service unavailable"));
assert!(handler.should_offer_fallback("Connection timeout"));
assert!(handler.should_offer_fallback("Network error"));
assert!(!handler.should_offer_fallback("Permission denied"));
}
#[test]
fn test_fallback_message() {
let temp_dir = TempDir::new().unwrap();
let handler = FallbackHandler::new(temp_dir.path());
let message = handler.fallback_message(ProviderType::PortalsCloud);
assert!(message.contains("Portals Cloud is currently unavailable"));
assert!(message.contains("Start a local Lore server instead"));
let message = handler.fallback_message(ProviderType::Remote);
assert!(message.contains("Remote Lore server is currently unavailable"));
let message = handler.fallback_message(ProviderType::Local);
assert!(message.contains("No fallback available"));
}
}