wowlab_parsers/parsers/dbc/
gametable.rs1use wowlab_fs::{directory, file, path::Path};
6
7use super::super::errors::DbcError;
8
9#[derive(Clone, Debug)]
11pub struct GameTable {
12 pub name: String,
13 pub columns: Vec<String>,
14 pub rows: Vec<Vec<String>>,
15}
16
17pub(super) fn parse_game_table(name: &str, text: &str) -> Result<GameTable, DbcError> {
18 let mut reader = csv::ReaderBuilder::new()
19 .delimiter(b'\t')
20 .flexible(true)
21 .trim(csv::Trim::All)
22 .from_reader(text.as_bytes());
23
24 let columns: Vec<String> = reader
25 .headers()
26 .map_err(|error| DbcError::csv_parse(name.to_string(), error))?
27 .iter()
28 .map(String::from)
29 .collect();
30
31 let mut rows = Vec::new();
32
33 for record in reader.records() {
34 let record = record.map_err(|error| DbcError::csv_parse(name.to_string(), error))?;
35
36 rows.push(record.iter().map(String::from).collect());
37 }
38
39 Ok(GameTable {
40 name: name.to_string(),
41 columns,
42 rows,
43 })
44}
45
46pub(crate) fn load_game_tables(tables_dir: &Path) -> Result<Vec<GameTable>, DbcError> {
48 let Ok(entries) = directory::entries(tables_dir) else {
49 return Ok(Vec::new());
50 };
51
52 let mut tables = Vec::new();
53
54 for entry in entries {
55 let path = entry.path();
56
57 if path.extension().and_then(|ext| ext.to_str()) != Some("txt") {
58 continue;
59 }
60
61 let Some(name) = path.file_stem().and_then(|stem| stem.to_str()) else {
62 continue;
63 };
64
65 let text = file::read_text(path)?;
66
67 if text.trim().is_empty() {
68 continue;
69 }
70
71 tables.push(parse_game_table(name, &text)?);
72 }
73
74 tables.sort_by(|a, b| a.name.cmp(&b.name));
75
76 Ok(tables)
77}
78
79#[cfg(test)]
80mod tests {
81 use googletest::prelude::*;
82 use insta::assert_debug_snapshot;
83
84 use super::*;
85
86 #[gtest]
87 fn parses_header_and_two_rows() -> Result<()> {
88 let table = parse_game_table("HpPerSta", "Level\tValue\n1\t2.5\n7\t9.0\n").or_fail()?;
89
90 verify_that!(table.name, eq("HpPerSta"))?;
91 verify_that!(
92 table.columns,
93 eq(&vec!["Level".to_string(), "Value".to_string()])
94 )?;
95 verify_that!(table.rows.len(), eq(2))?;
96 verify_that!(table.rows[0], eq(&vec!["1".to_string(), "2.5".to_string()]))?;
97
98 verify_that!(table.rows[1], eq(&vec!["7".to_string(), "9.0".to_string()]))
99 }
100
101 #[gtest]
102 fn trims_surrounding_whitespace() -> Result<()> {
103 let table = parse_game_table("Trimmed", " Level \t Value \n 1 \t 2.5 \n").or_fail()?;
104
105 verify_that!(
106 table.columns,
107 eq(&vec!["Level".to_string(), "Value".to_string()])
108 )?;
109
110 verify_that!(table.rows[0], eq(&vec!["1".to_string(), "2.5".to_string()]))
111 }
112
113 #[gtest]
114 fn flexible_width_allows_short_rows() -> Result<()> {
115 let result = parse_game_table("Flex", "A\tB\tC\n1\t2\n");
116
117 verify_that!(result.is_ok(), eq(true))?;
118 let table = result.or_fail()?;
119
120 verify_that!(table.rows[0].len(), eq(2))
121 }
122
123 #[gtest]
124 fn header_only_has_no_rows() -> Result<()> {
125 let table = parse_game_table("Empty", "Level\tValue\n").or_fail()?;
126
127 verify_that!(table.columns.len(), eq(2))?;
128
129 verify_that!(table.rows.is_empty(), eq(true))
130 }
131
132 #[gtest]
133 fn parsed_game_table_snapshot() -> Result<()> {
134 let table = parse_game_table("HpPerSta", "Level\tValue\n1\t2.5\n2\t3.0\n").or_fail()?;
135
136 assert_debug_snapshot!(table);
137
138 Ok(())
139 }
140}