bms_table/
lib.rs

1//! 示例程序
2//!
3
4#![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/// BMS难度表数据,看这一个就够了
18#[derive(Debug, Clone, PartialEq)]
19pub struct BmsTable {
20    /// 表格名称,如 "Satellite"
21    pub name: String,
22    /// 表格符号,如 "sl"
23    pub symbol: String,
24    /// 表格头文件的相对URL,如 "header.json"
25    pub header_url: Url,
26    /// 谱面数据文件的相对URL,如 "score.json"
27    pub data_url: Url,
28    /// 课程信息数组,每个元素是一个课程组的数组
29    pub course: Vec<Vec<CourseInfo>>,
30    /// 谱面数据
31    pub charts: Vec<ChartItem>,
32    /// 难度等级顺序,包含数字和字符串
33    pub level_order: Vec<String>,
34    /// 额外数据
35    pub extra: Value,
36}
37
38/// 从URL直接获取BmsTable对象
39///
40/// # 参数
41///
42/// * `url` - BMS表格HTML页面的URL
43///
44/// # 返回值
45///
46/// 返回解析后的BmsTable对象
47///
48/// # 错误
49///
50/// 如果无法获取数据或解析失败,将返回错误
51///
52/// # 示例
53///
54/// ```rust,no_run
55/// use bms_table::fetch_bms_table;
56///
57/// #[tokio::main]
58/// async fn main() -> anyhow::Result<()> {
59///     let bms_table = fetch_bms_table("https://example.com/table.html").await?;
60///     println!("表格名称: {}", bms_table.name);
61///     println!("谱面数据数量: {}", bms_table.charts.len());
62///     Ok(())
63/// }
64/// ```
65pub 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
70/// 从URL获取header的绝对URL地址、header和data的JSON解析树
71///
72/// # 参数
73///
74/// * `url` - BMS表格HTML页面的URL或直接指向JSON文件的URL
75///
76/// # 返回值
77///
78/// 返回一个元组,包含header的绝对URL地址、header的JSON解析树和data的JSON解析树
79///
80/// # 错误
81///
82/// 如果无法获取数据或解析失败,将返回错误
83///
84/// # 示例
85///
86/// ```rust,no_run
87/// use bms_table::fetch_table_json_data;
88///
89/// #[tokio::main]
90/// async fn main() -> anyhow::Result<()> {
91///     let (header_url, header_json, data_json) = fetch_table_json_data("https://example.com/table.html").await?;
92///     println!("Header URL: {}", header_url);
93///     println!("Header JSON: {:?}", header_json);
94///     println!("Data JSON: {:?}", data_json);
95///     Ok(())
96/// }
97/// ```
98pub 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    // 判断返回的内容是HTML还是JSON
103    let (header_url_str, header_json) = if is_json_content(&content) {
104        // 如果是JSON,直接当作header处理
105        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
122/// 从header的绝对URL地址、header和data的JSON解析树创建BmsTable对象
123///
124/// # 参数
125///
126/// * `header_url` - header文件的绝对URL地址
127/// * `header_json` - header的JSON解析树
128/// * `data_json` - data的JSON解析树
129///
130/// # 返回值
131///
132/// 返回解析后的BmsTable对象
133///
134/// # 错误
135///
136/// 如果JSON解析失败或URL解析失败,将返回错误
137///
138/// # 示例
139///
140/// ```rust,no_run
141/// use bms_table::{create_bms_table_from_json, BmsTable};
142/// use serde_json::json;
143/// use url::Url;
144///
145/// #[tokio::main]
146/// async fn main() -> anyhow::Result<()> {
147///     let header_url = "https://example.com/header.json";
148///     let header_json = json!({
149///         "name": "Test Table",
150///         "symbol": "test",
151///         "data_url": "score.json",
152///         "course": []
153///     });
154///     let data_json = json!([]);
155///     
156///     let bms_table = create_bms_table_from_json(header_url, header_json, data_json).await?;
157///     println!("表格名称: {}", bms_table.name);
158///     Ok(())
159/// }
160/// ```
161pub async fn create_bms_table_from_json(
162    header_url: &str,
163    header_json: Value,
164    data_json: Value,
165) -> Result<BmsTable> {
166    // 解析header JSON,保留额外数据
167    let header: BmsTableHeader = serde_json::from_value(header_json.clone())?;
168
169    // 提取额外数据(header_json中除了BmsTableHeader字段之外的数据)
170    let mut extra_data = header_json;
171    if let Some(obj) = extra_data.as_object_mut() {
172        // 移除已知字段,保留额外字段
173        obj.remove("name");
174        obj.remove("symbol");
175        obj.remove("data_url");
176        obj.remove("course");
177        obj.remove("level_order");
178    }
179
180    // 解析data JSON
181    let charts: Vec<ChartItem> = serde_json::from_value(data_json)?;
182
183    // 构建URL对象
184    let header_url_obj = Url::parse(header_url)?;
185    let data_url_obj = header_url_obj.join(&header.data_url)?;
186
187    // 创建BmsTable对象
188    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    /// 测试创建BmsTable对象
209    #[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        // 测试课程信息
265        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        // 测试谱面数据
277        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        // 测试额外数据
290        // 检查header的额外数据
291        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()); // 确保已知字段被移除
294
295        // 检查score的额外数据
296        assert_eq!(score.extra["custom_field"], "custom_value");
297        assert_eq!(score.extra["rating"], 5.0);
298        assert!(!score.extra.get("level").is_some()); // 确保已知字段被移除
299
300        // 测试level_order
301        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()); // 确保level_order被移除
306    }
307
308    /// 测试创建BmsTable对象时处理空字符串字段
309    #[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    /// 测试BmsTable结构体的基本功能
346    #[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    /// 测试BmsTable的PartialEq实现
367    #[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    /// 测试错误处理 - 无效的JSON
395    #[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            // 缺少必要的字段
403        });
404        let data_json = json!([
405            {
406                "level": "1",
407                "id": 1
408                // 缺少必要的字段
409            }
410        ]);
411
412        let result = create_bms_table_from_json(header_url, header_json, data_json).await;
413        assert!(result.is_ok()); // 这个测试应该通过,因为缺少的字段有默认值
414    }
415
416    /// 测试错误处理 - 无效的URL
417    #[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    /// 测试fetch_table_json_data函数的错误处理
433    #[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    /// 测试fetch_bms_table函数的错误处理
440    #[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    /// 测试URL解析功能
447    #[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    /// 测试JSON序列化和反序列化
459    #[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    /// 测试JSON内容判断功能
476    #[test]
477    fn test_is_json_content() {
478        // 测试JSON对象
479        assert!(is_json_content(r#"{"name": "test"}"#));
480        assert!(is_json_content(r#"  {"name": "test"}  "#));
481
482        // 测试JSON数组
483        assert!(is_json_content(r#"[1, 2, 3]"#));
484        assert!(is_json_content(r#"  [1, 2, 3]  "#));
485
486        // 测试非JSON内容
487        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}