use std::path::PathBuf;
use std::time::Duration;
use tracing::{debug, info, warn};
use ribbit_client::{ProductCdnsResponse, ProductVersionsResponse, Region as RibbitRegion};
use tact_client::Region as TactRegion;
use tact_client::http::{HttpClient, ProtocolVersion};
use crate::{Result, ensure_dir, get_cache_dir};
#[allow(dead_code)]
const DEFAULT_HTTP_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
#[allow(dead_code)]
const DEFAULT_RIBBIT_CACHE_TTL: Duration = Duration::from_secs(2 * 60);
pub struct HybridVersionClient {
http_client: HttpClient,
ribbit_client: Option<crate::cached_ribbit_client::CachedRibbitClient>,
#[allow(dead_code)]
cache_dir: PathBuf,
region: RibbitRegion,
enable_ribbit_fallback: bool,
}
impl HybridVersionClient {
pub async fn new(region: RibbitRegion) -> Result<Self> {
let tact_region = convert_ribbit_to_tact_region(region)?;
let http_client = HttpClient::new(tact_region, ProtocolVersion::V2)
.map_err(crate::Error::TactClient)?
.with_max_retries(2)
.with_user_agent("cascette-rs/0.3.1");
let ribbit_client = match crate::cached_ribbit_client::CachedRibbitClient::new(region).await
{
Ok(client) => Some(client),
Err(e) => {
warn!("Failed to create Ribbit fallback client: {}", e);
None
}
};
let cache_dir = get_cache_dir()?.join("hybrid");
ensure_dir(&cache_dir).await?;
let has_ribbit_fallback = ribbit_client.is_some();
info!(
"Initialized hybrid version client for region {:?} (HTTP primary, Ribbit fallback: {})",
region, has_ribbit_fallback
);
Ok(Self {
http_client,
ribbit_client,
cache_dir,
region,
enable_ribbit_fallback: has_ribbit_fallback,
})
}
pub async fn http_only(region: RibbitRegion) -> Result<Self> {
let tact_region = convert_ribbit_to_tact_region(region)?;
let http_client = HttpClient::new(tact_region, ProtocolVersion::V2)
.map_err(crate::Error::TactClient)?
.with_max_retries(3)
.with_user_agent("cascette-rs/0.3.1");
let cache_dir = get_cache_dir()?.join("hybrid");
ensure_dir(&cache_dir).await?;
info!(
"Initialized HTTP-only version client for region {:?}",
region
);
Ok(Self {
http_client,
ribbit_client: None,
cache_dir,
region,
enable_ribbit_fallback: false,
})
}
pub fn set_ribbit_fallback(&mut self, enabled: bool) {
self.enable_ribbit_fallback = enabled && self.ribbit_client.is_some();
}
pub async fn get_product_versions(&self, product: &str) -> Result<ProductVersionsResponse> {
debug!(
"Getting product versions for '{}' using hybrid approach",
product
);
match self.try_http_versions(product).await {
Ok(response) => {
info!("✓ Product versions retrieved via HTTPS for '{}'", product);
return Ok(response);
}
Err(e) => {
warn!("✗ HTTP version discovery failed for '{}': {}", product, e);
debug!("HTTP error details: {:?}", e);
}
}
if self.enable_ribbit_fallback {
if let Some(ref ribbit_client) = self.ribbit_client {
debug!("Falling back to Ribbit for product versions: '{}'", product);
match ribbit_client.get_product_versions(product).await {
Ok(response) => {
info!(
"✓ Product versions retrieved via Ribbit fallback for '{}'",
product
);
return Ok(response);
}
Err(e) => {
warn!("✗ Ribbit fallback also failed for '{}': {}", product, e);
}
}
}
}
Err(crate::Error::Network(format!(
"Both HTTP and Ribbit failed for product versions: {product}"
)))
}
pub async fn get_product_cdns(&self, product: &str) -> Result<ProductCdnsResponse> {
debug!(
"Getting CDN configuration for '{}' using hybrid approach",
product
);
match self.try_http_cdns(product).await {
Ok(response) => {
info!("✓ CDN configuration retrieved via HTTPS for '{}'", product);
return Ok(response);
}
Err(e) => {
warn!("✗ HTTP CDN discovery failed for '{}': {}", product, e);
debug!("HTTP error details: {:?}", e);
}
}
if self.enable_ribbit_fallback {
if let Some(ref ribbit_client) = self.ribbit_client {
debug!(
"Falling back to Ribbit for CDN configuration: '{}'",
product
);
match ribbit_client.get_product_cdns(product).await {
Ok(response) => {
info!(
"✓ CDN configuration retrieved via Ribbit fallback for '{}'",
product
);
return Ok(response);
}
Err(e) => {
warn!("✗ Ribbit fallback also failed for '{}': {}", product, e);
}
}
}
}
Err(crate::Error::Network(format!(
"Both HTTP and Ribbit failed for CDN configuration: {product}"
)))
}
async fn try_http_versions(&self, product: &str) -> Result<ProductVersionsResponse> {
let versions = self
.http_client
.get_product_versions_http_parsed(product)
.await
.map_err(crate::Error::TactClient)?;
Ok(ProductVersionsResponse {
sequence_number: None, entries: versions
.into_iter()
.map(|v| ribbit_client::VersionEntry {
region: v.region,
build_config: v.build_config,
cdn_config: v.cdn_config,
key_ring: v.key_ring,
build_id: v.build_id,
versions_name: v.versions_name,
product_config: v.product_config,
})
.collect(),
})
}
async fn try_http_cdns(&self, product: &str) -> Result<ProductCdnsResponse> {
let cdns = self
.http_client
.get_product_cdns_http_parsed(product)
.await
.map_err(crate::Error::TactClient)?;
Ok(ProductCdnsResponse {
sequence_number: None, entries: cdns
.into_iter()
.map(|c| ribbit_client::CdnEntry {
name: c.name,
path: c.path,
hosts: c.hosts,
servers: Vec::new(), config_path: c.config_path,
})
.collect(),
})
}
pub fn region(&self) -> RibbitRegion {
self.region
}
pub fn has_ribbit_fallback(&self) -> bool {
self.enable_ribbit_fallback && self.ribbit_client.is_some()
}
}
fn convert_ribbit_to_tact_region(region: RibbitRegion) -> Result<TactRegion> {
match region {
RibbitRegion::US => Ok(TactRegion::US),
RibbitRegion::EU => Ok(TactRegion::EU),
RibbitRegion::CN => Ok(TactRegion::CN),
RibbitRegion::KR => Ok(TactRegion::KR),
RibbitRegion::TW => Ok(TactRegion::TW),
RibbitRegion::SG => Err(crate::Error::Network(
"Singapore region not supported by TACT client".to_string(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_hybrid_client_creation() {
let client = HybridVersionClient::new(RibbitRegion::US).await;
assert!(client.is_ok(), "Should create hybrid client successfully");
let client = client.unwrap();
assert_eq!(client.region(), RibbitRegion::US);
}
#[tokio::test]
async fn test_http_only_client() {
let client = HybridVersionClient::http_only(RibbitRegion::EU).await;
assert!(
client.is_ok(),
"Should create HTTP-only client successfully"
);
let client = client.unwrap();
assert_eq!(client.region(), RibbitRegion::EU);
assert!(
!client.has_ribbit_fallback(),
"HTTP-only client should not have Ribbit fallback"
);
}
}