1use crate::error::{Error, Result};
7use sheetkit_xml::table::{
8 TableAutoFilter, TableColumnXml, TableColumnsXml, TableStyleInfoXml, TableXml,
9};
10use sheetkit_xml::worksheet::{AutoFilter, WorksheetXml};
11
12#[derive(Debug, Clone)]
14pub struct TableConfig {
15 pub name: String,
17 pub display_name: String,
19 pub range: String,
21 pub columns: Vec<TableColumn>,
23 pub show_header_row: bool,
25 pub style_name: Option<String>,
27 pub auto_filter: bool,
29 pub show_first_column: bool,
31 pub show_last_column: bool,
33 pub show_row_stripes: bool,
35 pub show_column_stripes: bool,
37}
38
39impl Default for TableConfig {
40 fn default() -> Self {
41 Self {
42 name: String::new(),
43 display_name: String::new(),
44 range: String::new(),
45 columns: Vec::new(),
46 show_header_row: true,
47 style_name: None,
48 auto_filter: true,
49 show_first_column: false,
50 show_last_column: false,
51 show_row_stripes: true,
52 show_column_stripes: false,
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct TableColumn {
60 pub name: String,
62 pub totals_row_function: Option<String>,
64 pub totals_row_label: Option<String>,
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct TableInfo {
71 pub name: String,
73 pub display_name: String,
75 pub range: String,
77 pub show_header_row: bool,
79 pub auto_filter: bool,
81 pub columns: Vec<String>,
83 pub style_name: Option<String>,
85}
86
87pub(crate) fn build_table_xml(config: &TableConfig, table_id: u32) -> TableXml {
89 let columns: Vec<TableColumnXml> = config
90 .columns
91 .iter()
92 .enumerate()
93 .map(|(i, col)| TableColumnXml {
94 id: (i + 1) as u32,
95 name: col.name.clone(),
96 totals_row_function: col.totals_row_function.clone(),
97 totals_row_label: col.totals_row_label.clone(),
98 })
99 .collect();
100
101 let auto_filter = if config.auto_filter {
102 Some(TableAutoFilter {
103 reference: config.range.clone(),
104 })
105 } else {
106 None
107 };
108
109 let style_info = if config.style_name.is_some()
110 || config.show_first_column
111 || config.show_last_column
112 || config.show_row_stripes
113 || config.show_column_stripes
114 {
115 Some(TableStyleInfoXml {
116 name: config.style_name.clone(),
117 show_first_column: Some(config.show_first_column),
118 show_last_column: Some(config.show_last_column),
119 show_row_stripes: Some(config.show_row_stripes),
120 show_column_stripes: Some(config.show_column_stripes),
121 })
122 } else {
123 None
124 };
125
126 let header_row_count = if !config.show_header_row {
127 Some(0)
128 } else {
129 None
130 };
131
132 TableXml {
133 xmlns: sheetkit_xml::namespaces::SPREADSHEET_ML.to_string(),
134 id: table_id,
135 name: config.name.clone(),
136 display_name: config.display_name.clone(),
137 reference: config.range.clone(),
138 totals_row_count: None,
139 totals_row_shown: None,
140 header_row_count,
141 auto_filter,
142 table_columns: TableColumnsXml {
143 count: columns.len() as u32,
144 columns,
145 },
146 table_style_info: style_info,
147 }
148}
149
150pub(crate) fn table_xml_to_info(table_xml: &TableXml) -> TableInfo {
152 let columns = table_xml
153 .table_columns
154 .columns
155 .iter()
156 .map(|c| c.name.clone())
157 .collect();
158 let show_header_row = table_xml.header_row_count != Some(0);
159 let auto_filter = table_xml.auto_filter.is_some();
160 let style_name = table_xml
161 .table_style_info
162 .as_ref()
163 .and_then(|s| s.name.clone());
164
165 TableInfo {
166 name: table_xml.name.clone(),
167 display_name: table_xml.display_name.clone(),
168 range: table_xml.reference.clone(),
169 show_header_row,
170 auto_filter,
171 columns,
172 style_name,
173 }
174}
175
176pub(crate) fn validate_table_config(config: &TableConfig) -> Result<()> {
178 if config.name.is_empty() {
179 return Err(Error::InvalidArgument("table name cannot be empty".into()));
180 }
181 if config.range.is_empty() {
182 return Err(Error::InvalidArgument("table range cannot be empty".into()));
183 }
184 if config.columns.is_empty() {
185 return Err(Error::InvalidArgument(
186 "table must have at least one column".into(),
187 ));
188 }
189 Ok(())
190}
191
192pub fn set_auto_filter(ws: &mut WorksheetXml, range: &str) -> Result<()> {
194 ws.auto_filter = Some(AutoFilter {
195 reference: range.to_string(),
196 });
197 Ok(())
198}
199
200pub fn remove_auto_filter(ws: &mut WorksheetXml) {
202 ws.auto_filter = None;
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_set_auto_filter() {
211 let mut ws = WorksheetXml::default();
212 set_auto_filter(&mut ws, "A1:D10").unwrap();
213
214 assert!(ws.auto_filter.is_some());
215 assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:D10");
216 }
217
218 #[test]
219 fn test_remove_auto_filter() {
220 let mut ws = WorksheetXml::default();
221 set_auto_filter(&mut ws, "A1:D10").unwrap();
222 remove_auto_filter(&mut ws);
223
224 assert!(ws.auto_filter.is_none());
225 }
226
227 #[test]
228 fn test_auto_filter_xml_roundtrip() {
229 let mut ws = WorksheetXml::default();
230 set_auto_filter(&mut ws, "A1:C100").unwrap();
231
232 let xml = quick_xml::se::to_string(&ws).unwrap();
233 assert!(xml.contains("autoFilter"));
234 assert!(xml.contains("A1:C100"));
235
236 let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
237 assert!(parsed.auto_filter.is_some());
238 assert_eq!(parsed.auto_filter.as_ref().unwrap().reference, "A1:C100");
239 }
240
241 #[test]
242 fn test_remove_auto_filter_when_none() {
243 let mut ws = WorksheetXml::default();
244 remove_auto_filter(&mut ws);
245 assert!(ws.auto_filter.is_none());
246 }
247
248 #[test]
249 fn test_overwrite_auto_filter() {
250 let mut ws = WorksheetXml::default();
251 set_auto_filter(&mut ws, "A1:B10").unwrap();
252 set_auto_filter(&mut ws, "A1:D20").unwrap();
253
254 assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:D20");
255 }
256
257 #[test]
258 fn test_table_config_creation() {
259 let config = TableConfig {
260 name: "Table1".to_string(),
261 display_name: "Table1".to_string(),
262 range: "A1:D10".to_string(),
263 columns: vec![
264 TableColumn {
265 name: "Name".to_string(),
266 totals_row_function: None,
267 totals_row_label: None,
268 },
269 TableColumn {
270 name: "Age".to_string(),
271 totals_row_function: None,
272 totals_row_label: None,
273 },
274 TableColumn {
275 name: "City".to_string(),
276 totals_row_function: None,
277 totals_row_label: None,
278 },
279 TableColumn {
280 name: "Score".to_string(),
281 totals_row_function: None,
282 totals_row_label: None,
283 },
284 ],
285 show_header_row: true,
286 style_name: Some("TableStyleMedium2".to_string()),
287 auto_filter: true,
288 ..TableConfig::default()
289 };
290
291 assert_eq!(config.name, "Table1");
292 assert_eq!(config.columns.len(), 4);
293 assert!(config.auto_filter);
294 }
295
296 #[test]
297 fn test_build_table_xml() {
298 let config = TableConfig {
299 name: "Sales".to_string(),
300 display_name: "Sales".to_string(),
301 range: "A1:C5".to_string(),
302 columns: vec![
303 TableColumn {
304 name: "Product".to_string(),
305 totals_row_function: None,
306 totals_row_label: None,
307 },
308 TableColumn {
309 name: "Quantity".to_string(),
310 totals_row_function: None,
311 totals_row_label: None,
312 },
313 TableColumn {
314 name: "Price".to_string(),
315 totals_row_function: None,
316 totals_row_label: None,
317 },
318 ],
319 show_header_row: true,
320 style_name: Some("TableStyleMedium2".to_string()),
321 auto_filter: true,
322 show_row_stripes: true,
323 ..TableConfig::default()
324 };
325
326 let table_xml = build_table_xml(&config, 1);
327 assert_eq!(table_xml.id, 1);
328 assert_eq!(table_xml.name, "Sales");
329 assert_eq!(table_xml.reference, "A1:C5");
330 assert_eq!(table_xml.table_columns.count, 3);
331 assert!(table_xml.auto_filter.is_some());
332 assert!(table_xml.table_style_info.is_some());
333 assert!(table_xml.header_row_count.is_none());
334 }
335
336 #[test]
337 fn test_build_table_xml_no_header() {
338 let config = TableConfig {
339 name: "T1".to_string(),
340 display_name: "T1".to_string(),
341 range: "A1:B5".to_string(),
342 columns: vec![TableColumn {
343 name: "Col1".to_string(),
344 totals_row_function: None,
345 totals_row_label: None,
346 }],
347 show_header_row: false,
348 ..TableConfig::default()
349 };
350
351 let table_xml = build_table_xml(&config, 2);
352 assert_eq!(table_xml.header_row_count, Some(0));
353 }
354
355 #[test]
356 fn test_table_xml_to_info() {
357 let table_xml = TableXml {
358 xmlns: sheetkit_xml::namespaces::SPREADSHEET_ML.to_string(),
359 id: 1,
360 name: "Inventory".to_string(),
361 display_name: "Inventory".to_string(),
362 reference: "A1:D20".to_string(),
363 totals_row_count: None,
364 totals_row_shown: None,
365 header_row_count: None,
366 auto_filter: Some(TableAutoFilter {
367 reference: "A1:D20".to_string(),
368 }),
369 table_columns: TableColumnsXml {
370 count: 4,
371 columns: vec![
372 TableColumnXml {
373 id: 1,
374 name: "Item".to_string(),
375 totals_row_function: None,
376 totals_row_label: None,
377 },
378 TableColumnXml {
379 id: 2,
380 name: "Stock".to_string(),
381 totals_row_function: None,
382 totals_row_label: None,
383 },
384 TableColumnXml {
385 id: 3,
386 name: "Price".to_string(),
387 totals_row_function: None,
388 totals_row_label: None,
389 },
390 TableColumnXml {
391 id: 4,
392 name: "Supplier".to_string(),
393 totals_row_function: None,
394 totals_row_label: None,
395 },
396 ],
397 },
398 table_style_info: Some(TableStyleInfoXml {
399 name: Some("TableStyleLight1".to_string()),
400 show_first_column: Some(false),
401 show_last_column: Some(false),
402 show_row_stripes: Some(true),
403 show_column_stripes: Some(false),
404 }),
405 };
406
407 let info = table_xml_to_info(&table_xml);
408 assert_eq!(info.name, "Inventory");
409 assert_eq!(info.display_name, "Inventory");
410 assert_eq!(info.range, "A1:D20");
411 assert!(info.show_header_row);
412 assert!(info.auto_filter);
413 assert_eq!(info.columns, vec!["Item", "Stock", "Price", "Supplier"]);
414 assert_eq!(info.style_name, Some("TableStyleLight1".to_string()));
415 }
416
417 #[test]
418 fn test_validate_table_config_empty_name() {
419 let config = TableConfig {
420 name: String::new(),
421 range: "A1:B5".to_string(),
422 columns: vec![TableColumn {
423 name: "Col".to_string(),
424 totals_row_function: None,
425 totals_row_label: None,
426 }],
427 ..TableConfig::default()
428 };
429 assert!(validate_table_config(&config).is_err());
430 }
431
432 #[test]
433 fn test_validate_table_config_empty_range() {
434 let config = TableConfig {
435 name: "T1".to_string(),
436 range: String::new(),
437 columns: vec![TableColumn {
438 name: "Col".to_string(),
439 totals_row_function: None,
440 totals_row_label: None,
441 }],
442 ..TableConfig::default()
443 };
444 assert!(validate_table_config(&config).is_err());
445 }
446
447 #[test]
448 fn test_validate_table_config_no_columns() {
449 let config = TableConfig {
450 name: "T1".to_string(),
451 range: "A1:B5".to_string(),
452 columns: vec![],
453 ..TableConfig::default()
454 };
455 assert!(validate_table_config(&config).is_err());
456 }
457
458 #[test]
459 fn test_validate_table_config_valid() {
460 let config = TableConfig {
461 name: "T1".to_string(),
462 display_name: "T1".to_string(),
463 range: "A1:B5".to_string(),
464 columns: vec![TableColumn {
465 name: "Col".to_string(),
466 totals_row_function: None,
467 totals_row_label: None,
468 }],
469 ..TableConfig::default()
470 };
471 assert!(validate_table_config(&config).is_ok());
472 }
473
474 #[test]
475 fn test_table_config_default() {
476 let config = TableConfig::default();
477 assert!(config.show_header_row);
478 assert!(config.auto_filter);
479 assert!(config.show_row_stripes);
480 assert!(!config.show_first_column);
481 assert!(!config.show_last_column);
482 assert!(!config.show_column_stripes);
483 }
484}