1#![warn(missing_docs)]
5
6mod fetch;
7
8use anyhow::{anyhow, Result};
9use serde_json::Value;
10use url::Url;
11
12use fetch::{extract_bmstable_url, BmsTableHeader, ChartItem, CourseInfo};
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct BmsTable {
17 pub name: String,
19 pub symbol: String,
21 pub header_url: Url,
23 pub data_url: Url,
25 pub course: Vec<Vec<CourseInfo>>,
27 pub charts: Vec<ChartItem>,
29 pub level_order: Vec<String>,
31 pub extra: serde_json::Value,
33}
34
35#[cfg(feature = "reqwest")]
63pub async fn fetch_bms_table(web_url: &str) -> Result<BmsTable> {
64 let web_url = Url::parse(web_url)?;
65 let web_response = reqwest::Client::new()
66 .get(web_url.clone())
67 .send()
68 .await
69 .map_err(|e| anyhow!("When fetching web: {e}"))?
70 .text()
71 .await
72 .map_err(|e| anyhow!("When parsing web response: {e}"))?;
73 let (header_url, header_json) = match get_web_header_json_value(&web_response)? {
74 HeaderQueryContent::Url(header_url_string) => {
75 let header_url = web_url.join(&header_url_string)?;
76 let header_response = reqwest::Client::new()
77 .get(header_url.clone())
78 .send()
79 .await
80 .map_err(|e| anyhow!("When fetching header: {e}"))?;
81 let header_response_string = header_response
82 .text()
83 .await
84 .map_err(|e| anyhow!("When parsing header response: {e}"))?;
85 let HeaderQueryContent::Json(header_json) =
86 get_web_header_json_value(&header_response_string)?
87 else {
88 return Err(anyhow!(
89 "Cycled header found. web_url: {web_url}, header_url: {header_url_string}"
90 ));
91 };
92 (header_url, header_json)
93 }
94 HeaderQueryContent::Json(value) => (web_url, value),
95 };
96 let data_url_str = header_json
97 .get("data_url")
98 .ok_or(anyhow!("\"data_url\" not found in header json!"))?
99 .as_str()
100 .ok_or(anyhow!("\"data_url\" is not a string!"))?;
101 let data_url = header_url.join(data_url_str)?;
102 let data_response = reqwest::Client::new()
103 .get(data_url)
104 .send()
105 .await
106 .map_err(|e| anyhow!("When fetching web: {e}"))?
107 .text()
108 .await
109 .map_err(|e| anyhow!("When parsing web response: {e}"))?;
110 let data_json: Value = serde_json::from_str(&data_response)?;
111 create_bms_table_from_json(header_url.as_str(), header_json, data_json)
112}
113
114pub enum HeaderQueryContent {
116 Url(String),
118 Json(Value),
120}
121
122pub fn get_web_header_json_value(response_str: &str) -> anyhow::Result<HeaderQueryContent> {
124 use crate::fetch::is_json_content;
125 if is_json_content(response_str) {
127 let header_json: Value = serde_json::from_str(response_str)
129 .map_err(|e| anyhow!("When parsing header json, Error: {e}"))?;
130 Ok(HeaderQueryContent::Json(header_json))
131 } else {
132 let bmstable_url = extract_bmstable_url(response_str)?;
133 Ok(HeaderQueryContent::Url(bmstable_url))
134 }
135}
136
137pub fn create_bms_table_from_json(
181 header_url: &str,
182 header_json: Value,
183 data_json: Value,
184) -> Result<BmsTable> {
185 let header: BmsTableHeader = serde_json::from_value(header_json.clone())?;
187
188 let mut extra_data = header_json;
190 if let Some(obj) = extra_data.as_object_mut() {
191 obj.remove("name");
193 obj.remove("symbol");
194 obj.remove("data_url");
195 obj.remove("course");
196 obj.remove("level_order");
197 }
198
199 let charts: Vec<ChartItem> = serde_json::from_value(data_json)?;
201
202 let header_url_obj = Url::parse(header_url)?;
204 let data_url_obj = header_url_obj.join(&header.data_url)?;
205
206 let bms_table = BmsTable {
208 name: header.name,
209 symbol: header.symbol,
210 header_url: header_url_obj,
211 data_url: data_url_obj,
212 course: header.course,
213 charts,
214 level_order: header.level_order,
215 extra: extra_data,
216 };
217
218 Ok(bms_table)
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::fetch::is_json_content;
225 use serde_json::json;
226 use url::Url;
227
228 #[test]
230 fn test_create_bms_table_from_json() {
231 let header_url = "https://example.com/header.json";
232 let header_json = json!({
233 "name": "Test Table",
234 "symbol": "test",
235 "data_url": "charts.json",
236 "course": [
237 [
238 {
239 "name": "Test Course",
240 "constraint": ["grade_mirror"],
241 "trophy": [
242 {
243 "name": "goldmedal",
244 "missrate": 1.0,
245 "scorerate": 90.0
246 }
247 ],
248 "md5": ["test_md5_1", "test_md5_2"]
249 }
250 ]
251 ],
252 "level_order": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, "!i"],
253 "extra_field": "extra_value",
254 "another_field": 123
255 });
256 let data_json = json!([
257 {
258 "level": "1",
259 "id": 1,
260 "md5": "test_md5_1",
261 "sha256": "test_sha256_1",
262 "title": "Test Song",
263 "artist": "Test Artist",
264 "url": "https://example.com/test.bms",
265 "url_diff": "https://example.com/test_diff.bms",
266 "custom_field": "custom_value",
267 "rating": 5.0
268 }
269 ]);
270
271 let result = create_bms_table_from_json(header_url, header_json, data_json);
272 assert!(result.is_ok());
273
274 let bms_table = result.unwrap();
275 assert_eq!(bms_table.name, "Test Table");
276 assert_eq!(bms_table.symbol, "test");
277 assert_eq!(
278 bms_table.data_url.as_str(),
279 "https://example.com/charts.json"
280 );
281 assert_eq!(bms_table.course.len(), 1);
282 assert_eq!(bms_table.charts.len(), 1);
283
284 let course = &bms_table.course[0][0];
286 assert_eq!(course.name, "Test Course");
287 assert_eq!(course.constraint, vec!["grade_mirror"]);
288 assert_eq!(course.trophy.len(), 1);
289 assert_eq!(course.trophy[0].name, "goldmedal");
290 assert_eq!(course.trophy[0].missrate, 1.0);
291 assert_eq!(course.trophy[0].scorerate, 90.0);
292 assert_eq!(course.charts.len(), 2);
293 assert_eq!(course.charts[0].md5, Some("test_md5_1".to_string()));
294 assert_eq!(course.charts[1].md5, Some("test_md5_2".to_string()));
295
296 let score = &bms_table.charts[0];
298 assert_eq!(score.level, "1");
299 assert_eq!(score.md5, Some("test_md5_1".to_string()));
300 assert_eq!(score.sha256, Some("test_sha256_1".to_string()));
301 assert_eq!(score.title, Some("Test Song".to_string()));
302 assert_eq!(score.artist, Some("Test Artist".to_string()));
303 assert_eq!(score.url, Some("https://example.com/test.bms".to_string()));
304 assert_eq!(
305 score.url_diff,
306 Some("https://example.com/test_diff.bms".to_string())
307 );
308
309 assert_eq!(bms_table.extra["extra_field"], "extra_value");
312 assert_eq!(bms_table.extra["another_field"], 123);
313 assert!(bms_table.extra.get("name").is_none()); assert_eq!(score.extra["custom_field"], "custom_value");
317 assert_eq!(score.extra["rating"], 5.0);
318 assert!(score.extra.get("level").is_none()); assert_eq!(bms_table.level_order.len(), 22);
322 assert_eq!(bms_table.level_order[0], "0");
323 assert_eq!(bms_table.level_order[20], "20");
324 assert_eq!(bms_table.level_order[21], "!i");
325 assert!(bms_table.extra.get("level_order").is_none()); }
327
328 #[test]
330 fn test_create_bms_table_with_empty_fields() {
331 let header_url = "https://example.com/header.json";
332 let header_json = json!({
333 "name": "Test Table",
334 "symbol": "test",
335 "data_url": "charts.json",
336 "course": []
337 });
338 let data_json = json!([
339 {
340 "level": "1",
341 "id": 1,
342 "md5": "",
343 "sha256": "",
344 "title": "",
345 "artist": "",
346 "url": "",
347 "url_diff": ""
348 }
349 ]);
350
351 let result = create_bms_table_from_json(header_url, header_json, data_json);
352 assert!(result.is_ok());
353
354 let bms_table = result.unwrap();
355 let score = &bms_table.charts[0];
356 assert_eq!(score.level, "1");
357 assert_eq!(score.md5, None);
358 assert_eq!(score.sha256, None);
359 assert_eq!(score.title, None);
360 assert_eq!(score.artist, None);
361 assert_eq!(score.url, None);
362 assert_eq!(score.url_diff, None);
363 }
364
365 #[test]
367 fn test_bms_table_creation() {
368 let bms_table = BmsTable {
369 name: "Test Table".to_string(),
370 symbol: "test".to_string(),
371 header_url: Url::parse("https://example.com/header.json").unwrap(),
372 data_url: Url::parse("https://example.com/charts.json").unwrap(),
373 course: vec![],
374 charts: vec![],
375 level_order: vec!["0".to_string(), "1".to_string()],
376 extra: json!({}),
377 };
378
379 assert_eq!(bms_table.name, "Test Table");
380 assert_eq!(bms_table.symbol, "test");
381 assert_eq!(bms_table.course.len(), 0);
382 assert_eq!(bms_table.charts.len(), 0);
383 assert_eq!(bms_table.level_order.len(), 2);
384 }
385
386 #[test]
388 fn test_bms_table_partial_eq() {
389 let table1 = BmsTable {
390 name: "Test Table".to_string(),
391 symbol: "test".to_string(),
392 header_url: Url::parse("https://example.com/header.json").unwrap(),
393 data_url: Url::parse("https://example.com/charts.json").unwrap(),
394 course: vec![],
395 charts: vec![],
396 level_order: vec!["0".to_string(), "1".to_string()],
397 extra: json!({}),
398 };
399
400 let table2 = BmsTable {
401 name: "Test Table".to_string(),
402 symbol: "test".to_string(),
403 header_url: Url::parse("https://example.com/header.json").unwrap(),
404 data_url: Url::parse("https://example.com/charts.json").unwrap(),
405 course: vec![],
406 charts: vec![],
407 level_order: vec!["0".to_string(), "1".to_string()],
408 extra: json!({}),
409 };
410
411 assert_eq!(table1, table2);
412 }
413
414 #[test]
416 fn test_create_bms_table_invalid_json() {
417 let header_url = "https://example.com/header.json";
418 let header_json = json!({
419 "name": "Test Table",
420 "symbol": "test",
421 "data_url": "charts.json"
422 });
424 let data_json = json!([
425 {
426 "level": "1",
427 "id": 1
428 }
430 ]);
431
432 let result = create_bms_table_from_json(header_url, header_json, data_json);
433 assert!(result.is_ok()); }
435
436 #[test]
438 fn test_create_bms_table_invalid_url() {
439 let header_url = "invalid-url";
440 let header_json = json!({
441 "name": "Test Table",
442 "symbol": "test",
443 "data_url": "charts.json",
444 "course": []
445 });
446 let data_json = json!([]);
447
448 let result = create_bms_table_from_json(header_url, header_json, data_json);
449 assert!(result.is_err());
450 }
451
452 #[tokio::test]
454 #[cfg(feature = "reqwest")]
455 async fn test_fetch_table_json_data_invalid_url() {
456 let result = fetch_bms_table("https://invalid-url-that-does-not-exist.com").await;
457 assert!(result.is_err());
458 }
459
460 #[tokio::test]
462 #[cfg(feature = "reqwest")]
463 async fn test_fetch_bms_table_invalid_url() {
464 let result = fetch_bms_table("https://invalid-url-that-does-not-exist.com").await;
465 assert!(result.is_err());
466 }
467
468 #[test]
470 fn test_url_parsing() {
471 let base_url = "https://example.com/table.html";
472 let bmstable_url = "header.json";
473
474 let base_url_obj = Url::parse(base_url).unwrap();
475 let header_url = base_url_obj.join(bmstable_url).unwrap();
476
477 assert_eq!(header_url.as_str(), "https://example.com/header.json");
478 }
479
480 #[test]
482 fn test_json_serialization() {
483 let header = BmsTableHeader {
484 name: "Test Table".to_string(),
485 symbol: "test".to_string(),
486 data_url: "charts.json".to_string(),
487 course: vec![],
488 level_order: vec!["0".to_string(), "1".to_string(), "!i".to_string()],
489 };
490
491 let json = serde_json::to_string(&header).unwrap();
492 let parsed: BmsTableHeader = serde_json::from_str(&json).unwrap();
493
494 assert_eq!(header, parsed);
495 }
496
497 #[test]
499 fn test_is_json_content() {
500 assert!(is_json_content(r#"{"name": "test"}"#));
502 assert!(is_json_content(r#" {"name": "test"} "#));
503
504 assert!(is_json_content(r#"[1, 2, 3]"#));
506 assert!(is_json_content(r#" [1, 2, 3] "#));
507
508 assert!(!is_json_content("<html><body>test</body></html>"));
510 assert!(!is_json_content("This is plain text"));
511 assert!(!is_json_content(""));
512 assert!(!is_json_content(" "));
513 }
514}