1use crate::{Error, Response, error::Result};
7use ngdp_bpsv::BpsvDocument;
8
9pub trait TypedResponse: Sized {
11 fn from_bpsv(doc: &BpsvDocument) -> Result<Self>;
16
17 fn from_response(response: &Response) -> Result<Self> {
22 match &response.data {
23 Some(data) => {
24 let doc = BpsvDocument::parse(data)
26 .map_err(|e| Error::ParseError(format!("BPSV parse error: {e}")))?;
27 Self::from_bpsv(&doc)
28 }
29 None => Err(Error::ParseError("No data in response".to_string())),
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct ProductVersionsResponse {
37 pub sequence_number: Option<u32>,
39 pub entries: Vec<VersionEntry>,
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct VersionEntry {
46 pub region: String,
48 pub build_config: String,
50 pub cdn_config: String,
52 pub key_ring: Option<String>,
54 pub build_id: u32,
56 pub versions_name: String,
58 pub product_config: String,
60}
61
62#[derive(Debug, Clone, PartialEq)]
64pub struct ProductCdnsResponse {
65 pub sequence_number: Option<u32>,
67 pub entries: Vec<CdnEntry>,
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub struct CdnEntry {
74 pub name: String,
76 pub path: String,
78 pub hosts: Vec<String>,
80 pub servers: Vec<String>,
82 pub config_path: String,
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub struct ProductBgdlResponse {
89 pub sequence_number: Option<u32>,
91 pub entries: Vec<BgdlEntry>,
93}
94
95#[derive(Debug, Clone, PartialEq)]
97pub struct BgdlEntry {
98 pub region: String,
100 pub build_config: String,
102 pub cdn_config: String,
104 pub install_bgdl_config: Option<String>,
106 pub game_bgdl_config: Option<String>,
108}
109
110#[derive(Debug, Clone, PartialEq)]
112pub struct SummaryResponse {
113 pub sequence_number: Option<u32>,
115 pub products: Vec<ProductSummary>,
117}
118
119#[derive(Debug, Clone, PartialEq)]
121pub struct ProductSummary {
122 pub product: String,
124 pub seqn: u32,
126 pub flags: Option<String>,
128}
129
130struct FieldAccessor<'a> {
132 row: &'a ngdp_bpsv::document::BpsvRow<'a>,
133 schema: &'a ngdp_bpsv::BpsvSchema,
134}
135
136impl<'a> FieldAccessor<'a> {
137 fn new(row: &'a ngdp_bpsv::document::BpsvRow, schema: &'a ngdp_bpsv::BpsvSchema) -> Self {
138 Self { row, schema }
139 }
140
141 fn get_string(&self, field: &str) -> Result<String> {
142 self.row
143 .get_raw_by_name(field, self.schema)
144 .map(std::string::ToString::to_string)
145 .ok_or_else(|| Error::ParseError(format!("Missing field: {field}")))
146 }
147
148 fn get_string_optional(&self, field: &str) -> Option<String> {
149 self.row.get_raw_by_name(field, self.schema).and_then(|s| {
150 if s.is_empty() {
151 None
152 } else {
153 Some(s.to_string())
154 }
155 })
156 }
157
158 fn get_u32(&self, field: &str) -> Result<u32> {
159 let value = self.get_string(field)?;
160 value
161 .parse()
162 .map_err(|_| Error::ParseError(format!("Invalid u32 for {field}: {value}")))
163 }
164
165 fn get_string_list(&self, field: &str, separator: char) -> Result<Vec<String>> {
166 let value = self.get_string(field)?;
167 if value.is_empty() {
168 Ok(Vec::new())
169 } else {
170 Ok(value
171 .split(separator)
172 .map(|s| s.trim().to_string())
173 .collect())
174 }
175 }
176}
177
178impl TypedResponse for ProductVersionsResponse {
181 fn from_bpsv(doc: &BpsvDocument) -> Result<Self> {
182 let mut entries = Vec::new();
183 let schema = doc.schema();
184
185 for row in doc.rows() {
186 let accessor = FieldAccessor::new(row, schema);
187
188 entries.push(VersionEntry {
189 region: accessor.get_string("Region")?,
190 build_config: accessor.get_string("BuildConfig")?,
191 cdn_config: accessor.get_string("CDNConfig")?,
192 key_ring: accessor.get_string_optional("KeyRing"),
193 build_id: accessor.get_u32("BuildId")?,
194 versions_name: accessor.get_string("VersionsName")?,
195 product_config: accessor.get_string("ProductConfig")?,
196 });
197 }
198
199 Ok(Self {
200 sequence_number: doc.sequence_number(),
201 entries,
202 })
203 }
204}
205
206impl TypedResponse for ProductCdnsResponse {
207 fn from_bpsv(doc: &BpsvDocument) -> Result<Self> {
208 let mut entries = Vec::new();
209 let schema = doc.schema();
210
211 for row in doc.rows() {
212 let accessor = FieldAccessor::new(row, schema);
213
214 entries.push(CdnEntry {
215 name: accessor.get_string("Name")?,
216 path: accessor.get_string("Path")?,
217 hosts: accessor.get_string_list("Hosts", ' ')?,
218 servers: accessor
219 .get_string_optional("Servers")
220 .map(|s| {
221 s.split_whitespace()
222 .map(std::string::ToString::to_string)
223 .collect::<Vec<_>>()
224 })
225 .unwrap_or_default(),
226 config_path: accessor.get_string("ConfigPath")?,
227 });
228 }
229
230 Ok(Self {
231 sequence_number: doc.sequence_number(),
232 entries,
233 })
234 }
235}
236
237impl TypedResponse for ProductBgdlResponse {
238 fn from_bpsv(doc: &BpsvDocument) -> Result<Self> {
239 let mut entries = Vec::new();
240 let schema = doc.schema();
241
242 for row in doc.rows() {
243 let accessor = FieldAccessor::new(row, schema);
244
245 entries.push(BgdlEntry {
246 region: accessor.get_string("Region")?,
247 build_config: accessor.get_string("BuildConfig")?,
248 cdn_config: accessor.get_string("CDNConfig")?,
249 install_bgdl_config: accessor.get_string_optional("InstallBGDLConfig"),
250 game_bgdl_config: accessor.get_string_optional("GameBGDLConfig"),
251 });
252 }
253
254 Ok(Self {
255 sequence_number: doc.sequence_number(),
256 entries,
257 })
258 }
259}
260
261impl TypedResponse for SummaryResponse {
262 fn from_bpsv(doc: &BpsvDocument) -> Result<Self> {
263 let mut products = Vec::new();
264 let schema = doc.schema();
265
266 for row in doc.rows() {
267 let accessor = FieldAccessor::new(row, schema);
268
269 products.push(ProductSummary {
270 product: accessor.get_string("Product")?,
271 seqn: accessor.get_u32("Seqn")?,
272 flags: accessor.get_string_optional("Flags"),
273 });
274 }
275
276 Ok(Self {
277 sequence_number: doc.sequence_number(),
278 products,
279 })
280 }
281}
282
283impl ProductVersionsResponse {
285 #[must_use]
287 pub fn get_region(&self, region: &str) -> Option<&VersionEntry> {
288 self.entries.iter().find(|e| e.region == region)
289 }
290
291 #[must_use]
293 pub fn build_ids(&self) -> Vec<u32> {
294 let mut ids: Vec<_> = self.entries.iter().map(|e| e.build_id).collect();
295 ids.sort_unstable();
296 ids.dedup();
297 ids
298 }
299}
300
301impl ProductCdnsResponse {
302 #[must_use]
304 pub fn get_cdn(&self, name: &str) -> Option<&CdnEntry> {
305 self.entries.iter().find(|e| e.name == name)
306 }
307
308 #[must_use]
310 pub fn all_hosts(&self) -> Vec<String> {
311 let mut hosts = Vec::new();
312 for entry in &self.entries {
313 hosts.extend(entry.hosts.clone());
314 }
315 hosts.sort();
316 hosts.dedup();
317 hosts
318 }
319}
320
321impl SummaryResponse {
322 #[must_use]
324 pub fn get_product(&self, product: &str) -> Option<&ProductSummary> {
325 self.products.iter().find(|p| p.product == product)
326 }
327
328 #[must_use]
330 pub fn product_codes(&self) -> Vec<&str> {
331 self.products.iter().map(|p| p.product.as_str()).collect()
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_parse_product_versions() {
341 let bpsv_data = concat!(
343 "Region!STRING:0|BuildConfig!HEX:16|CDNConfig!HEX:16|BuildId!DEC:4|VersionsName!STRING:0|ProductConfig!HEX:16\n",
344 "## seqn = 12345\n",
345 "us|abcdef1234567890abcdef1234567890|fedcba0987654321fedcba0987654321|123456|10.2.5.53040|1234567890abcdef1234567890abcdef\n",
346 "eu|abcdef1234567890abcdef1234567890|fedcba0987654321fedcba0987654321|123456|10.2.5.53040|1234567890abcdef1234567890abcdef\n"
347 );
348
349 let doc = BpsvDocument::parse(bpsv_data).unwrap();
350 let response = ProductVersionsResponse::from_bpsv(&doc).unwrap();
351
352 assert_eq!(response.sequence_number, Some(12345));
353 assert_eq!(response.entries.len(), 2);
354 assert_eq!(response.entries[0].region, "us");
355 assert_eq!(response.entries[0].build_id, 123_456);
356 assert_eq!(response.entries[0].versions_name, "10.2.5.53040");
357 }
358
359 #[test]
360 fn test_parse_product_cdns() {
361 let bpsv_data = concat!(
362 "Name!STRING:0|Path!STRING:0|Hosts!STRING:0|ConfigPath!STRING:0\n",
363 "## seqn = 54321\n",
364 "us|tpr/wow|level3.blizzard.com edgecast.blizzard.com|tpr/configs/data\n",
365 "eu|tpr/wow|level3.blizzard.com|tpr/configs/data\n"
366 );
367
368 let doc = BpsvDocument::parse(bpsv_data).unwrap();
369 let response = ProductCdnsResponse::from_bpsv(&doc).unwrap();
370
371 assert_eq!(response.sequence_number, Some(54321));
372 assert_eq!(response.entries.len(), 2);
373 assert_eq!(response.entries[0].name, "us");
374 assert_eq!(response.entries[0].hosts.len(), 2);
375 assert_eq!(response.entries[0].hosts[0], "level3.blizzard.com");
376 }
377
378 #[test]
379 fn test_parse_summary() {
380 let bpsv_data = concat!(
381 "Product!STRING:0|Seqn!DEC:4|Flags!STRING:0\n",
382 "## seqn = 99999\n",
383 "wow|12345|installed\n",
384 "d3|54321|\n",
385 "hero|11111|beta\n"
386 );
387
388 let doc = BpsvDocument::parse(bpsv_data).unwrap();
389 let response = SummaryResponse::from_bpsv(&doc).unwrap();
390
391 assert_eq!(response.sequence_number, Some(99999));
392 assert_eq!(response.products.len(), 3);
393 assert_eq!(response.products[0].product, "wow");
394 assert_eq!(response.products[0].seqn, 12345);
395 assert_eq!(response.products[0].flags, Some("installed".to_string()));
396 assert_eq!(response.products[1].flags, None);
397 }
398
399 #[test]
400 fn test_from_response_with_hex_adjustment() {
401 let data = concat!(
403 "Region!STRING:0|BuildConfig!HEX:16\n",
404 "## seqn = 12345\n",
405 "us|e359107662e72559b4e1ab721b157cb0\n"
406 );
407
408 let response = Response {
409 raw: data.as_bytes().to_vec(),
410 data: Some(data.to_string()),
411 mime_parts: None,
412 };
413
414 let result = ProductVersionsResponse::from_response(&response);
417
418 assert!(result.is_err());
421 let err_msg = result.unwrap_err().to_string();
422 assert!(err_msg.contains("Missing field") || err_msg.contains("Parse error"));
423 assert!(!err_msg.contains("Invalid value for field 'BuildConfig'"));
424 }
425}