ngdp_cache/
hybrid_version_client.rs1use std::path::PathBuf;
26use std::time::Duration;
27use tracing::{debug, info, warn};
28
29use ribbit_client::{ProductCdnsResponse, ProductVersionsResponse, Region as RibbitRegion};
30use tact_client::Region as TactRegion;
31use tact_client::http::{HttpClient, ProtocolVersion};
32
33use crate::{Result, ensure_dir, get_cache_dir};
34
35#[allow(dead_code)]
37const DEFAULT_HTTP_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
38
39#[allow(dead_code)]
41const DEFAULT_RIBBIT_CACHE_TTL: Duration = Duration::from_secs(2 * 60);
42
43pub struct HybridVersionClient {
45 http_client: HttpClient,
47 ribbit_client: Option<crate::cached_ribbit_client::CachedRibbitClient>,
49 #[allow(dead_code)]
51 cache_dir: PathBuf,
52 region: RibbitRegion,
54 enable_ribbit_fallback: bool,
56}
57
58impl HybridVersionClient {
59 pub async fn new(region: RibbitRegion) -> Result<Self> {
61 let tact_region = convert_ribbit_to_tact_region(region)?;
63 let http_client = HttpClient::new(tact_region, ProtocolVersion::V2)
65 .map_err(crate::Error::TactClient)?
66 .with_max_retries(2)
67 .with_user_agent("cascette-rs/0.3.1");
68
69 let ribbit_client = match crate::cached_ribbit_client::CachedRibbitClient::new(region).await
71 {
72 Ok(client) => Some(client),
73 Err(e) => {
74 warn!("Failed to create Ribbit fallback client: {}", e);
75 None
76 }
77 };
78
79 let cache_dir = get_cache_dir()?.join("hybrid");
80 ensure_dir(&cache_dir).await?;
81
82 let has_ribbit_fallback = ribbit_client.is_some();
83 info!(
84 "Initialized hybrid version client for region {:?} (HTTP primary, Ribbit fallback: {})",
85 region, has_ribbit_fallback
86 );
87
88 Ok(Self {
89 http_client,
90 ribbit_client,
91 cache_dir,
92 region,
93 enable_ribbit_fallback: has_ribbit_fallback,
94 })
95 }
96
97 pub async fn http_only(region: RibbitRegion) -> Result<Self> {
99 let tact_region = convert_ribbit_to_tact_region(region)?;
100 let http_client = HttpClient::new(tact_region, ProtocolVersion::V2)
101 .map_err(crate::Error::TactClient)?
102 .with_max_retries(3)
103 .with_user_agent("cascette-rs/0.3.1");
104
105 let cache_dir = get_cache_dir()?.join("hybrid");
106 ensure_dir(&cache_dir).await?;
107
108 info!(
109 "Initialized HTTP-only version client for region {:?}",
110 region
111 );
112
113 Ok(Self {
114 http_client,
115 ribbit_client: None,
116 cache_dir,
117 region,
118 enable_ribbit_fallback: false,
119 })
120 }
121
122 pub fn set_ribbit_fallback(&mut self, enabled: bool) {
124 self.enable_ribbit_fallback = enabled && self.ribbit_client.is_some();
125 }
126
127 pub async fn get_product_versions(&self, product: &str) -> Result<ProductVersionsResponse> {
129 debug!(
130 "Getting product versions for '{}' using hybrid approach",
131 product
132 );
133
134 match self.try_http_versions(product).await {
136 Ok(response) => {
137 info!("✓ Product versions retrieved via HTTPS for '{}'", product);
138 return Ok(response);
139 }
140 Err(e) => {
141 warn!("✗ HTTP version discovery failed for '{}': {}", product, e);
142 debug!("HTTP error details: {:?}", e);
143 }
144 }
145
146 if self.enable_ribbit_fallback {
148 if let Some(ref ribbit_client) = self.ribbit_client {
149 debug!("Falling back to Ribbit for product versions: '{}'", product);
150 match ribbit_client.get_product_versions(product).await {
151 Ok(response) => {
152 info!(
153 "✓ Product versions retrieved via Ribbit fallback for '{}'",
154 product
155 );
156 return Ok(response);
157 }
158 Err(e) => {
159 warn!("✗ Ribbit fallback also failed for '{}': {}", product, e);
160 }
161 }
162 }
163 }
164
165 Err(crate::Error::Network(format!(
166 "Both HTTP and Ribbit failed for product versions: {product}"
167 )))
168 }
169
170 pub async fn get_product_cdns(&self, product: &str) -> Result<ProductCdnsResponse> {
172 debug!(
173 "Getting CDN configuration for '{}' using hybrid approach",
174 product
175 );
176
177 match self.try_http_cdns(product).await {
179 Ok(response) => {
180 info!("✓ CDN configuration retrieved via HTTPS for '{}'", product);
181 return Ok(response);
182 }
183 Err(e) => {
184 warn!("✗ HTTP CDN discovery failed for '{}': {}", product, e);
185 debug!("HTTP error details: {:?}", e);
186 }
187 }
188
189 if self.enable_ribbit_fallback {
191 if let Some(ref ribbit_client) = self.ribbit_client {
192 debug!(
193 "Falling back to Ribbit for CDN configuration: '{}'",
194 product
195 );
196 match ribbit_client.get_product_cdns(product).await {
197 Ok(response) => {
198 info!(
199 "✓ CDN configuration retrieved via Ribbit fallback for '{}'",
200 product
201 );
202 return Ok(response);
203 }
204 Err(e) => {
205 warn!("✗ Ribbit fallback also failed for '{}': {}", product, e);
206 }
207 }
208 }
209 }
210
211 Err(crate::Error::Network(format!(
212 "Both HTTP and Ribbit failed for CDN configuration: {product}"
213 )))
214 }
215
216 async fn try_http_versions(&self, product: &str) -> Result<ProductVersionsResponse> {
218 let versions = self
219 .http_client
220 .get_product_versions_http_parsed(product)
221 .await
222 .map_err(crate::Error::TactClient)?;
223
224 Ok(ProductVersionsResponse {
226 sequence_number: None, entries: versions
228 .into_iter()
229 .map(|v| ribbit_client::VersionEntry {
230 region: v.region,
231 build_config: v.build_config,
232 cdn_config: v.cdn_config,
233 key_ring: v.key_ring,
234 build_id: v.build_id,
235 versions_name: v.versions_name,
236 product_config: v.product_config,
237 })
238 .collect(),
239 })
240 }
241
242 async fn try_http_cdns(&self, product: &str) -> Result<ProductCdnsResponse> {
244 let cdns = self
245 .http_client
246 .get_product_cdns_http_parsed(product)
247 .await
248 .map_err(crate::Error::TactClient)?;
249
250 Ok(ProductCdnsResponse {
252 sequence_number: None, entries: cdns
254 .into_iter()
255 .map(|c| ribbit_client::CdnEntry {
256 name: c.name,
257 path: c.path,
258 hosts: c.hosts,
259 servers: Vec::new(), config_path: c.config_path,
261 })
262 .collect(),
263 })
264 }
265
266 pub fn region(&self) -> RibbitRegion {
268 self.region
269 }
270
271 pub fn has_ribbit_fallback(&self) -> bool {
273 self.enable_ribbit_fallback && self.ribbit_client.is_some()
274 }
275}
276
277fn convert_ribbit_to_tact_region(region: RibbitRegion) -> Result<TactRegion> {
279 match region {
280 RibbitRegion::US => Ok(TactRegion::US),
281 RibbitRegion::EU => Ok(TactRegion::EU),
282 RibbitRegion::CN => Ok(TactRegion::CN),
283 RibbitRegion::KR => Ok(TactRegion::KR),
284 RibbitRegion::TW => Ok(TactRegion::TW),
285 RibbitRegion::SG => Err(crate::Error::Network(
286 "Singapore region not supported by TACT client".to_string(),
287 )),
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[tokio::test]
296 async fn test_hybrid_client_creation() {
297 let client = HybridVersionClient::new(RibbitRegion::US).await;
298 assert!(client.is_ok(), "Should create hybrid client successfully");
299
300 let client = client.unwrap();
301 assert_eq!(client.region(), RibbitRegion::US);
302 }
303
304 #[tokio::test]
305 async fn test_http_only_client() {
306 let client = HybridVersionClient::http_only(RibbitRegion::EU).await;
307 assert!(
308 client.is_ok(),
309 "Should create HTTP-only client successfully"
310 );
311
312 let client = client.unwrap();
313 assert_eq!(client.region(), RibbitRegion::EU);
314 assert!(
315 !client.has_ribbit_fallback(),
316 "HTTP-only client should not have Ribbit fallback"
317 );
318 }
319}