1#![warn(missing_docs)]
5
6mod fetch;
7
8use anyhow::Result;
9use reqwest::Client;
10use serde_json::Value;
11use url::Url;
12
13use crate::fetch::{
14 extract_bmstable_url, fetch_data_json, is_json_content, BmsTableHeader, ChartItem, CourseInfo,
15};
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct BmsTable {
20 pub name: String,
22 pub symbol: String,
24 pub header_url: Url,
26 pub data_url: Url,
28 pub course: Vec<Vec<CourseInfo>>,
30 pub charts: Vec<ChartItem>,
32 pub level_order: Vec<String>,
34 pub extra: Value,
36}
37
38pub async fn fetch_bms_table(url: &str) -> Result<BmsTable> {
66 let (header_url, header_json, data_json) = fetch_table_json_data(url).await?;
67 create_bms_table_from_json(&header_url, header_json, data_json).await
68}
69
70pub async fn fetch_table_json_data(url: &str) -> Result<(String, Value, Value)> {
99 let response = Client::new().get(url).send().await?;
100 let content = response.text().await?;
101
102 let (header_url_str, header_json) = if is_json_content(&content) {
104 let header_json: Value = serde_json::from_str(&content)?;
106 (url.to_string(), header_json)
107 } else {
108 let bmstable_url = extract_bmstable_url(&content).await?;
109 let base_url_obj = Url::parse(url)?;
110 let header_url = base_url_obj.join(&bmstable_url)?;
111 let header_response = Client::new().get(header_url.as_str()).send().await?;
112 let header_json_content = header_response.text().await?;
113 let header_json: Value = serde_json::from_str(&header_json_content)?;
114 (header_url.to_string(), header_json)
115 };
116
117 let data_json = fetch_data_json(&header_json, &header_url_str).await?;
118
119 Ok((header_url_str, header_json, data_json))
120}
121
122pub async fn create_bms_table_from_json(
162 header_url: &str,
163 header_json: Value,
164 data_json: Value,
165) -> Result<BmsTable> {
166 let header: BmsTableHeader = serde_json::from_value(header_json.clone())?;
168
169 let mut extra_data = header_json;
171 if let Some(obj) = extra_data.as_object_mut() {
172 obj.remove("name");
174 obj.remove("symbol");
175 obj.remove("data_url");
176 obj.remove("course");
177 obj.remove("level_order");
178 }
179
180 let charts: Vec<ChartItem> = serde_json::from_value(data_json)?;
182
183 let header_url_obj = Url::parse(header_url)?;
185 let data_url_obj = header_url_obj.join(&header.data_url)?;
186
187 let bms_table = BmsTable {
189 name: header.name,
190 symbol: header.symbol,
191 header_url: header_url_obj,
192 data_url: data_url_obj,
193 course: header.course,
194 charts,
195 level_order: header.level_order,
196 extra: extra_data,
197 };
198
199 Ok(bms_table)
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use serde_json::json;
206 use url::Url;
207
208 #[tokio::test]
210 async fn test_create_bms_table_from_json() {
211 let header_url = "https://example.com/header.json";
212 let header_json = json!({
213 "name": "Test Table",
214 "symbol": "test",
215 "data_url": "charts.json",
216 "course": [
217 [
218 {
219 "name": "Test Course",
220 "constraint": ["grade_mirror"],
221 "trophy": [
222 {
223 "name": "goldmedal",
224 "missrate": 1.0,
225 "scorerate": 90.0
226 }
227 ],
228 "md5": ["test_md5_1", "test_md5_2"]
229 }
230 ]
231 ],
232 "level_order": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, "!i"],
233 "extra_field": "extra_value",
234 "another_field": 123
235 });
236 let data_json = json!([
237 {
238 "level": "1",
239 "id": 1,
240 "md5": "test_md5_1",
241 "sha256": "test_sha256_1",
242 "title": "Test Song",
243 "artist": "Test Artist",
244 "url": "https://example.com/test.bms",
245 "url_diff": "https://example.com/test_diff.bms",
246 "custom_field": "custom_value",
247 "rating": 5.0
248 }
249 ]);
250
251 let result = create_bms_table_from_json(header_url, header_json, data_json).await;
252 assert!(result.is_ok());
253
254 let bms_table = result.unwrap();
255 assert_eq!(bms_table.name, "Test Table");
256 assert_eq!(bms_table.symbol, "test");
257 assert_eq!(
258 bms_table.data_url.as_str(),
259 "https://example.com/charts.json"
260 );
261 assert_eq!(bms_table.course.len(), 1);
262 assert_eq!(bms_table.charts.len(), 1);
263
264 let course = &bms_table.course[0][0];
266 assert_eq!(course.name, "Test Course");
267 assert_eq!(course.constraint, vec!["grade_mirror"]);
268 assert_eq!(course.trophy.len(), 1);
269 assert_eq!(course.trophy[0].name, "goldmedal");
270 assert_eq!(course.trophy[0].missrate, 1.0);
271 assert_eq!(course.trophy[0].scorerate, 90.0);
272 assert_eq!(course.charts.len(), 2);
273 assert_eq!(course.charts[0].md5, Some("test_md5_1".to_string()));
274 assert_eq!(course.charts[1].md5, Some("test_md5_2".to_string()));
275
276 let score = &bms_table.charts[0];
278 assert_eq!(score.level, "1");
279 assert_eq!(score.md5, Some("test_md5_1".to_string()));
280 assert_eq!(score.sha256, Some("test_sha256_1".to_string()));
281 assert_eq!(score.title, Some("Test Song".to_string()));
282 assert_eq!(score.artist, Some("Test Artist".to_string()));
283 assert_eq!(score.url, Some("https://example.com/test.bms".to_string()));
284 assert_eq!(
285 score.url_diff,
286 Some("https://example.com/test_diff.bms".to_string())
287 );
288
289 assert_eq!(bms_table.extra["extra_field"], "extra_value");
292 assert_eq!(bms_table.extra["another_field"], 123);
293 assert!(!bms_table.extra.get("name").is_some()); assert_eq!(score.extra["custom_field"], "custom_value");
297 assert_eq!(score.extra["rating"], 5.0);
298 assert!(!score.extra.get("level").is_some()); assert_eq!(bms_table.level_order.len(), 22);
302 assert_eq!(bms_table.level_order[0], "0");
303 assert_eq!(bms_table.level_order[20], "20");
304 assert_eq!(bms_table.level_order[21], "!i");
305 assert!(!bms_table.extra.get("level_order").is_some()); }
307
308 #[tokio::test]
310 async fn test_create_bms_table_with_empty_fields() {
311 let header_url = "https://example.com/header.json";
312 let header_json = json!({
313 "name": "Test Table",
314 "symbol": "test",
315 "data_url": "charts.json",
316 "course": []
317 });
318 let data_json = json!([
319 {
320 "level": "1",
321 "id": 1,
322 "md5": "",
323 "sha256": "",
324 "title": "",
325 "artist": "",
326 "url": "",
327 "url_diff": ""
328 }
329 ]);
330
331 let result = create_bms_table_from_json(header_url, header_json, data_json).await;
332 assert!(result.is_ok());
333
334 let bms_table = result.unwrap();
335 let score = &bms_table.charts[0];
336 assert_eq!(score.level, "1");
337 assert_eq!(score.md5, None);
338 assert_eq!(score.sha256, None);
339 assert_eq!(score.title, None);
340 assert_eq!(score.artist, None);
341 assert_eq!(score.url, None);
342 assert_eq!(score.url_diff, None);
343 }
344
345 #[test]
347 fn test_bms_table_creation() {
348 let bms_table = BmsTable {
349 name: "Test Table".to_string(),
350 symbol: "test".to_string(),
351 header_url: Url::parse("https://example.com/header.json").unwrap(),
352 data_url: Url::parse("https://example.com/charts.json").unwrap(),
353 course: vec![],
354 charts: vec![],
355 level_order: vec!["0".to_string(), "1".to_string()],
356 extra: json!({}),
357 };
358
359 assert_eq!(bms_table.name, "Test Table");
360 assert_eq!(bms_table.symbol, "test");
361 assert_eq!(bms_table.course.len(), 0);
362 assert_eq!(bms_table.charts.len(), 0);
363 assert_eq!(bms_table.level_order.len(), 2);
364 }
365
366 #[test]
368 fn test_bms_table_partial_eq() {
369 let table1 = BmsTable {
370 name: "Test Table".to_string(),
371 symbol: "test".to_string(),
372 header_url: Url::parse("https://example.com/header.json").unwrap(),
373 data_url: Url::parse("https://example.com/charts.json").unwrap(),
374 course: vec![],
375 charts: vec![],
376 level_order: vec!["0".to_string(), "1".to_string()],
377 extra: json!({}),
378 };
379
380 let table2 = BmsTable {
381 name: "Test Table".to_string(),
382 symbol: "test".to_string(),
383 header_url: Url::parse("https://example.com/header.json").unwrap(),
384 data_url: Url::parse("https://example.com/charts.json").unwrap(),
385 course: vec![],
386 charts: vec![],
387 level_order: vec!["0".to_string(), "1".to_string()],
388 extra: json!({}),
389 };
390
391 assert_eq!(table1, table2);
392 }
393
394 #[tokio::test]
396 async fn test_create_bms_table_invalid_json() {
397 let header_url = "https://example.com/header.json";
398 let header_json = json!({
399 "name": "Test Table",
400 "symbol": "test",
401 "data_url": "charts.json"
402 });
404 let data_json = json!([
405 {
406 "level": "1",
407 "id": 1
408 }
410 ]);
411
412 let result = create_bms_table_from_json(header_url, header_json, data_json).await;
413 assert!(result.is_ok()); }
415
416 #[tokio::test]
418 async fn test_create_bms_table_invalid_url() {
419 let header_url = "invalid-url";
420 let header_json = json!({
421 "name": "Test Table",
422 "symbol": "test",
423 "data_url": "charts.json",
424 "course": []
425 });
426 let data_json = json!([]);
427
428 let result = create_bms_table_from_json(header_url, header_json, data_json).await;
429 assert!(result.is_err());
430 }
431
432 #[tokio::test]
434 async fn test_fetch_table_json_data_invalid_url() {
435 let result = fetch_table_json_data("https://invalid-url-that-does-not-exist.com").await;
436 assert!(result.is_err());
437 }
438
439 #[tokio::test]
441 async fn test_fetch_bms_table_invalid_url() {
442 let result = fetch_bms_table("https://invalid-url-that-does-not-exist.com").await;
443 assert!(result.is_err());
444 }
445
446 #[test]
448 fn test_url_parsing() {
449 let base_url = "https://example.com/table.html";
450 let bmstable_url = "header.json";
451
452 let base_url_obj = Url::parse(base_url).unwrap();
453 let header_url = base_url_obj.join(bmstable_url).unwrap();
454
455 assert_eq!(header_url.as_str(), "https://example.com/header.json");
456 }
457
458 #[test]
460 fn test_json_serialization() {
461 let header = BmsTableHeader {
462 name: "Test Table".to_string(),
463 symbol: "test".to_string(),
464 data_url: "charts.json".to_string(),
465 course: vec![],
466 level_order: vec!["0".to_string(), "1".to_string(), "!i".to_string()],
467 };
468
469 let json = serde_json::to_string(&header).unwrap();
470 let parsed: BmsTableHeader = serde_json::from_str(&json).unwrap();
471
472 assert_eq!(header, parsed);
473 }
474
475 #[test]
477 fn test_is_json_content() {
478 assert!(is_json_content(r#"{"name": "test"}"#));
480 assert!(is_json_content(r#" {"name": "test"} "#));
481
482 assert!(is_json_content(r#"[1, 2, 3]"#));
484 assert!(is_json_content(r#" [1, 2, 3] "#));
485
486 assert!(!is_json_content("<html><body>test</body></html>"));
488 assert!(!is_json_content("This is plain text"));
489 assert!(!is_json_content(""));
490 assert!(!is_json_content(" "));
491 }
492}