bms_table/
lib.rs

1//! 示例程序
2//!
3
4#![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/// BMS难度表数据,看这一个就够了
15#[derive(Debug, Clone, PartialEq)]
16pub struct BmsTable {
17    /// 表格名称,如 "Satellite"
18    pub name: String,
19    /// 表格符号,如 "sl"
20    pub symbol: String,
21    /// 表格头文件的相对URL,如 "header.json"
22    pub header_url: Url,
23    /// 谱面数据文件的相对URL,如 "score.json"
24    pub data_url: Url,
25    /// 课程信息数组,每个元素是一个课程组的数组
26    pub course: Vec<Vec<CourseInfo>>,
27    /// 谱面数据
28    pub charts: Vec<ChartItem>,
29    /// 难度等级顺序,包含数字和字符串
30    pub level_order: Vec<String>,
31    /// 额外数据
32    pub extra: serde_json::Value,
33}
34
35/// 从URL直接获取BmsTable对象
36///
37/// # 参数
38///
39/// * `url` - BMS表格HTML页面的URL
40///
41/// # 返回值
42///
43/// 返回解析后的BmsTable对象
44///
45/// # 错误
46///
47/// 如果无法获取数据或解析失败,将返回错误
48///
49/// # 示例
50///
51/// ```rust,no_run
52/// use bms_table::fetch_bms_table;
53///
54/// #[tokio::main]
55/// async fn main() -> anyhow::Result<()> {
56///     let bms_table = fetch_bms_table("https://example.com/table.html").await?;
57///     println!("表格名称: {}", bms_table.name);
58///     println!("谱面数据数量: {}", bms_table.charts.len());
59///     Ok(())
60/// }
61/// ```
62#[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
114/// [`get_web_header_json_value`]的返回类型
115pub enum HeaderQueryContent {
116    /// 注意:可能解析出相对或绝对Url,建议使用[`Url::join`]。
117    Url(String),
118    /// Json树
119    Json(Value),
120}
121
122/// 从相应数据中提取Json树(Json内容)或Header地址(HTML内容)
123pub fn get_web_header_json_value(response_str: &str) -> anyhow::Result<HeaderQueryContent> {
124    use crate::fetch::is_json_content;
125    // 判断返回的内容是HTML还是JSON
126    if is_json_content(response_str) {
127        // 如果是JSON,直接当作header处理
128        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
137/// 从header的绝对URL地址、header和data的JSON解析树创建BmsTable对象
138///
139/// # 参数
140///
141/// * `header_url` - header文件的绝对URL地址
142/// * `header_json` - header的JSON解析树
143/// * `data_json` - data的JSON解析树
144///
145/// # 返回值
146///
147/// 返回解析后的BmsTable对象
148///
149/// # 错误
150///
151/// 如果JSON解析失败或URL解析失败,将返回错误
152///
153/// # 示例
154///
155/// ```rust,no_run
156/// use bms_table::{create_bms_table_from_json, BmsTable};
157/// use serde_json::json;
158/// use url::Url;
159///
160/// #[tokio::main]
161/// #[cfg(feature = "reqwest")]
162/// async fn main() -> anyhow::Result<()> {
163///     let header_url = "https://example.com/header.json";
164///     let header_json = json!({
165///         "name": "Test Table",
166///         "symbol": "test",
167///         "data_url": "score.json",
168///         "course": []
169///     });
170///     let data_json = json!([]);
171///     
172///     let bms_table = create_bms_table_from_json(header_url, header_json, data_json)?;
173///     println!("表格名称: {}", bms_table.name);
174///     Ok(())
175/// }
176///
177/// #[cfg(not(feature = "reqwest"))]
178/// fn main() {}
179/// ```
180pub fn create_bms_table_from_json(
181    header_url: &str,
182    header_json: Value,
183    data_json: Value,
184) -> Result<BmsTable> {
185    // 解析header JSON,保留额外数据
186    let header: BmsTableHeader = serde_json::from_value(header_json.clone())?;
187
188    // 提取额外数据(header_json中除了BmsTableHeader字段之外的数据)
189    let mut extra_data = header_json;
190    if let Some(obj) = extra_data.as_object_mut() {
191        // 移除已知字段,保留额外字段
192        obj.remove("name");
193        obj.remove("symbol");
194        obj.remove("data_url");
195        obj.remove("course");
196        obj.remove("level_order");
197    }
198
199    // 解析data JSON
200    let charts: Vec<ChartItem> = serde_json::from_value(data_json)?;
201
202    // 构建URL对象
203    let header_url_obj = Url::parse(header_url)?;
204    let data_url_obj = header_url_obj.join(&header.data_url)?;
205
206    // 创建BmsTable对象
207    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    /// 测试创建BmsTable对象
229    #[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        // 测试课程信息
285        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        // 测试谱面数据
297        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        // 测试额外数据
310        // 检查header的额外数据
311        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()); // 确保已知字段被移除
314
315        // 检查score的额外数据
316        assert_eq!(score.extra["custom_field"], "custom_value");
317        assert_eq!(score.extra["rating"], 5.0);
318        assert!(score.extra.get("level").is_none()); // 确保已知字段被移除
319
320        // 测试level_order
321        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()); // 确保level_order被移除
326    }
327
328    /// 测试创建BmsTable对象时处理空字符串字段
329    #[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    /// 测试BmsTable结构体的基本功能
366    #[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    /// 测试BmsTable的PartialEq实现
387    #[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    /// 测试错误处理 - 无效的JSON
415    #[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            // 缺少必要的字段
423        });
424        let data_json = json!([
425            {
426                "level": "1",
427                "id": 1
428                // 缺少必要的字段
429            }
430        ]);
431
432        let result = create_bms_table_from_json(header_url, header_json, data_json);
433        assert!(result.is_ok()); // 这个测试应该通过,因为缺少的字段有默认值
434    }
435
436    /// 测试错误处理 - 无效的URL
437    #[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    /// 测试fetch_table_json_data函数的错误处理
453    #[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    /// 测试fetch_bms_table函数的错误处理
461    #[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    /// 测试URL解析功能
469    #[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    /// 测试JSON序列化和反序列化
481    #[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    /// 测试JSON内容判断功能
498    #[test]
499    fn test_is_json_content() {
500        // 测试JSON对象
501        assert!(is_json_content(r#"{"name": "test"}"#));
502        assert!(is_json_content(r#"  {"name": "test"}  "#));
503
504        // 测试JSON数组
505        assert!(is_json_content(r#"[1, 2, 3]"#));
506        assert!(is_json_content(r#"  [1, 2, 3]  "#));
507
508        // 测试非JSON内容
509        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}