Skip to main content

sheetkit_xml/
table.rs

1//! Table XML schema structures.
2//!
3//! Represents `xl/tables/table{N}.xml` in the OOXML package.
4//! Tables define structured data ranges with named columns, auto-filters,
5//! and optional styles.
6
7use serde::{Deserialize, Serialize};
8
9use crate::namespaces;
10
11/// Root element for a table definition part (`xl/tables/table{N}.xml`).
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(rename = "table")]
14pub struct TableXml {
15    #[serde(rename = "@xmlns")]
16    pub xmlns: String,
17
18    #[serde(rename = "@id")]
19    pub id: u32,
20
21    #[serde(rename = "@name")]
22    pub name: String,
23
24    #[serde(rename = "@displayName")]
25    pub display_name: String,
26
27    #[serde(rename = "@ref")]
28    pub reference: String,
29
30    #[serde(rename = "@totalsRowCount", skip_serializing_if = "Option::is_none")]
31    pub totals_row_count: Option<u32>,
32
33    #[serde(rename = "@totalsRowShown", skip_serializing_if = "Option::is_none")]
34    pub totals_row_shown: Option<bool>,
35
36    #[serde(rename = "@headerRowCount", skip_serializing_if = "Option::is_none")]
37    pub header_row_count: Option<u32>,
38
39    #[serde(rename = "autoFilter", skip_serializing_if = "Option::is_none")]
40    pub auto_filter: Option<TableAutoFilter>,
41
42    #[serde(rename = "tableColumns")]
43    pub table_columns: TableColumnsXml,
44
45    #[serde(rename = "tableStyleInfo", skip_serializing_if = "Option::is_none")]
46    pub table_style_info: Option<TableStyleInfoXml>,
47}
48
49/// Auto-filter reference within a table definition.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct TableAutoFilter {
52    #[serde(rename = "@ref")]
53    pub reference: String,
54}
55
56/// Container for table column definitions.
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58pub struct TableColumnsXml {
59    #[serde(rename = "@count")]
60    pub count: u32,
61
62    #[serde(rename = "tableColumn", default)]
63    pub columns: Vec<TableColumnXml>,
64}
65
66/// A single column within a table definition.
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct TableColumnXml {
69    #[serde(rename = "@id")]
70    pub id: u32,
71
72    #[serde(rename = "@name")]
73    pub name: String,
74
75    #[serde(rename = "@totalsRowFunction", skip_serializing_if = "Option::is_none")]
76    pub totals_row_function: Option<String>,
77
78    #[serde(rename = "@totalsRowLabel", skip_serializing_if = "Option::is_none")]
79    pub totals_row_label: Option<String>,
80}
81
82/// Style information for a table.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct TableStyleInfoXml {
85    #[serde(rename = "@name", skip_serializing_if = "Option::is_none")]
86    pub name: Option<String>,
87
88    #[serde(rename = "@showFirstColumn", skip_serializing_if = "Option::is_none")]
89    pub show_first_column: Option<bool>,
90
91    #[serde(rename = "@showLastColumn", skip_serializing_if = "Option::is_none")]
92    pub show_last_column: Option<bool>,
93
94    #[serde(rename = "@showRowStripes", skip_serializing_if = "Option::is_none")]
95    pub show_row_stripes: Option<bool>,
96
97    #[serde(rename = "@showColumnStripes", skip_serializing_if = "Option::is_none")]
98    pub show_column_stripes: Option<bool>,
99}
100
101impl Default for TableXml {
102    fn default() -> Self {
103        Self {
104            xmlns: namespaces::SPREADSHEET_ML.to_string(),
105            id: 1,
106            name: "Table1".to_string(),
107            display_name: "Table1".to_string(),
108            reference: "A1:A1".to_string(),
109            totals_row_count: None,
110            totals_row_shown: None,
111            header_row_count: None,
112            auto_filter: None,
113            table_columns: TableColumnsXml {
114                count: 1,
115                columns: vec![TableColumnXml {
116                    id: 1,
117                    name: "Column1".to_string(),
118                    totals_row_function: None,
119                    totals_row_label: None,
120                }],
121            },
122            table_style_info: None,
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_table_xml_default() {
133        let table = TableXml::default();
134        assert_eq!(table.xmlns, namespaces::SPREADSHEET_ML);
135        assert_eq!(table.name, "Table1");
136        assert_eq!(table.display_name, "Table1");
137        assert_eq!(table.reference, "A1:A1");
138        assert_eq!(table.table_columns.count, 1);
139        assert_eq!(table.table_columns.columns.len(), 1);
140    }
141
142    #[test]
143    fn test_table_xml_serialize_roundtrip() {
144        let table = TableXml {
145            xmlns: namespaces::SPREADSHEET_ML.to_string(),
146            id: 1,
147            name: "SalesTable".to_string(),
148            display_name: "SalesTable".to_string(),
149            reference: "A1:D10".to_string(),
150            totals_row_count: None,
151            totals_row_shown: None,
152            header_row_count: None,
153            auto_filter: Some(TableAutoFilter {
154                reference: "A1:D10".to_string(),
155            }),
156            table_columns: TableColumnsXml {
157                count: 4,
158                columns: vec![
159                    TableColumnXml {
160                        id: 1,
161                        name: "Name".to_string(),
162                        totals_row_function: None,
163                        totals_row_label: None,
164                    },
165                    TableColumnXml {
166                        id: 2,
167                        name: "Region".to_string(),
168                        totals_row_function: None,
169                        totals_row_label: None,
170                    },
171                    TableColumnXml {
172                        id: 3,
173                        name: "Sales".to_string(),
174                        totals_row_function: None,
175                        totals_row_label: None,
176                    },
177                    TableColumnXml {
178                        id: 4,
179                        name: "Profit".to_string(),
180                        totals_row_function: None,
181                        totals_row_label: None,
182                    },
183                ],
184            },
185            table_style_info: Some(TableStyleInfoXml {
186                name: Some("TableStyleMedium2".to_string()),
187                show_first_column: Some(false),
188                show_last_column: Some(false),
189                show_row_stripes: Some(true),
190                show_column_stripes: Some(false),
191            }),
192        };
193
194        let xml = quick_xml::se::to_string(&table).unwrap();
195        assert!(xml.contains("SalesTable"));
196        assert!(xml.contains("A1:D10"));
197        assert!(xml.contains("autoFilter"));
198        assert!(xml.contains("TableStyleMedium2"));
199
200        let parsed: TableXml = quick_xml::de::from_str(&xml).unwrap();
201        assert_eq!(parsed.name, "SalesTable");
202        assert_eq!(parsed.display_name, "SalesTable");
203        assert_eq!(parsed.reference, "A1:D10");
204        assert_eq!(parsed.table_columns.count, 4);
205        assert_eq!(parsed.table_columns.columns.len(), 4);
206        assert_eq!(parsed.table_columns.columns[0].name, "Name");
207        assert!(parsed.auto_filter.is_some());
208        assert_eq!(parsed.auto_filter.unwrap().reference, "A1:D10");
209        let style = parsed.table_style_info.unwrap();
210        assert_eq!(style.name, Some("TableStyleMedium2".to_string()));
211        assert_eq!(style.show_row_stripes, Some(true));
212    }
213
214    #[test]
215    fn test_table_xml_without_optional_fields() {
216        let table = TableXml {
217            xmlns: namespaces::SPREADSHEET_ML.to_string(),
218            id: 2,
219            name: "Table2".to_string(),
220            display_name: "Table2".to_string(),
221            reference: "B1:C5".to_string(),
222            totals_row_count: None,
223            totals_row_shown: None,
224            header_row_count: None,
225            auto_filter: None,
226            table_columns: TableColumnsXml {
227                count: 2,
228                columns: vec![
229                    TableColumnXml {
230                        id: 1,
231                        name: "Col1".to_string(),
232                        totals_row_function: None,
233                        totals_row_label: None,
234                    },
235                    TableColumnXml {
236                        id: 2,
237                        name: "Col2".to_string(),
238                        totals_row_function: None,
239                        totals_row_label: None,
240                    },
241                ],
242            },
243            table_style_info: None,
244        };
245
246        let xml = quick_xml::se::to_string(&table).unwrap();
247        assert!(!xml.contains("autoFilter"));
248        assert!(!xml.contains("tableStyleInfo"));
249
250        let parsed: TableXml = quick_xml::de::from_str(&xml).unwrap();
251        assert_eq!(parsed.id, 2);
252        assert!(parsed.auto_filter.is_none());
253        assert!(parsed.table_style_info.is_none());
254    }
255
256    #[test]
257    fn test_table_xml_with_totals_row() {
258        let table = TableXml {
259            xmlns: namespaces::SPREADSHEET_ML.to_string(),
260            id: 3,
261            name: "Table3".to_string(),
262            display_name: "Table3".to_string(),
263            reference: "A1:B5".to_string(),
264            totals_row_count: Some(1),
265            totals_row_shown: Some(true),
266            header_row_count: None,
267            auto_filter: None,
268            table_columns: TableColumnsXml {
269                count: 2,
270                columns: vec![
271                    TableColumnXml {
272                        id: 1,
273                        name: "Label".to_string(),
274                        totals_row_function: None,
275                        totals_row_label: Some("Total".to_string()),
276                    },
277                    TableColumnXml {
278                        id: 2,
279                        name: "Amount".to_string(),
280                        totals_row_function: Some("sum".to_string()),
281                        totals_row_label: None,
282                    },
283                ],
284            },
285            table_style_info: None,
286        };
287
288        let xml = quick_xml::se::to_string(&table).unwrap();
289        assert!(xml.contains("totalsRowCount"));
290        assert!(xml.contains("totalsRowFunction"));
291        assert!(xml.contains("totalsRowLabel"));
292
293        let parsed: TableXml = quick_xml::de::from_str(&xml).unwrap();
294        assert_eq!(parsed.totals_row_count, Some(1));
295        assert_eq!(
296            parsed.table_columns.columns[0].totals_row_label,
297            Some("Total".to_string())
298        );
299        assert_eq!(
300            parsed.table_columns.columns[1].totals_row_function,
301            Some("sum".to_string())
302        );
303    }
304
305    #[test]
306    fn test_parse_real_excel_table_xml() {
307        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
308<table xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
309       id="1" name="Table1" displayName="Table1" ref="A1:C4">
310  <autoFilter ref="A1:C4"/>
311  <tableColumns count="3">
312    <tableColumn id="1" name="Name"/>
313    <tableColumn id="2" name="City"/>
314    <tableColumn id="3" name="Score"/>
315  </tableColumns>
316  <tableStyleInfo name="TableStyleMedium9" showFirstColumn="0" showLastColumn="0"
317                  showRowStripes="1" showColumnStripes="0"/>
318</table>"#;
319
320        let parsed: TableXml = quick_xml::de::from_str(xml).unwrap();
321        assert_eq!(parsed.id, 1);
322        assert_eq!(parsed.name, "Table1");
323        assert_eq!(parsed.display_name, "Table1");
324        assert_eq!(parsed.reference, "A1:C4");
325        assert!(parsed.auto_filter.is_some());
326        assert_eq!(parsed.auto_filter.unwrap().reference, "A1:C4");
327        assert_eq!(parsed.table_columns.count, 3);
328        assert_eq!(parsed.table_columns.columns[0].name, "Name");
329        assert_eq!(parsed.table_columns.columns[1].name, "City");
330        assert_eq!(parsed.table_columns.columns[2].name, "Score");
331        let style = parsed.table_style_info.unwrap();
332        assert_eq!(style.name, Some("TableStyleMedium9".to_string()));
333        assert_eq!(style.show_first_column, Some(false));
334        assert_eq!(style.show_row_stripes, Some(true));
335    }
336}