Skip to main content

sheetkit_xml/
workbook.rs

1//! Workbook XML schema structures.
2//!
3//! Represents `xl/workbook.xml` in the OOXML package.
4
5use serde::{Deserialize, Serialize};
6
7use crate::namespaces;
8
9/// Workbook root element (`xl/workbook.xml`).
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(rename = "workbook")]
12pub struct WorkbookXml {
13    #[serde(rename = "@xmlns")]
14    pub xmlns: String,
15
16    #[serde(rename = "@xmlns:r")]
17    pub xmlns_r: String,
18
19    #[serde(rename = "fileVersion", skip_serializing_if = "Option::is_none")]
20    pub file_version: Option<FileVersion>,
21
22    #[serde(rename = "workbookPr", skip_serializing_if = "Option::is_none")]
23    pub workbook_pr: Option<WorkbookPr>,
24
25    #[serde(rename = "workbookProtection", skip_serializing_if = "Option::is_none")]
26    pub workbook_protection: Option<WorkbookProtection>,
27
28    #[serde(rename = "bookViews", skip_serializing_if = "Option::is_none")]
29    pub book_views: Option<BookViews>,
30
31    #[serde(rename = "sheets")]
32    pub sheets: Sheets,
33
34    #[serde(rename = "definedNames", skip_serializing_if = "Option::is_none")]
35    pub defined_names: Option<DefinedNames>,
36
37    #[serde(rename = "calcPr", skip_serializing_if = "Option::is_none")]
38    pub calc_pr: Option<CalcPr>,
39
40    #[serde(rename = "pivotCaches", skip_serializing_if = "Option::is_none")]
41    pub pivot_caches: Option<PivotCaches>,
42}
43
44/// File version information.
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct FileVersion {
47    #[serde(rename = "@appName", skip_serializing_if = "Option::is_none")]
48    pub app_name: Option<String>,
49
50    #[serde(rename = "@lastEdited", skip_serializing_if = "Option::is_none")]
51    pub last_edited: Option<String>,
52
53    #[serde(rename = "@lowestEdited", skip_serializing_if = "Option::is_none")]
54    pub lowest_edited: Option<String>,
55
56    #[serde(rename = "@rupBuild", skip_serializing_if = "Option::is_none")]
57    pub rup_build: Option<String>,
58}
59
60/// Workbook properties.
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub struct WorkbookPr {
63    #[serde(rename = "@date1904", skip_serializing_if = "Option::is_none")]
64    pub date1904: Option<bool>,
65
66    #[serde(rename = "@filterPrivacy", skip_serializing_if = "Option::is_none")]
67    pub filter_privacy: Option<bool>,
68
69    #[serde(
70        rename = "@defaultThemeVersion",
71        skip_serializing_if = "Option::is_none"
72    )]
73    pub default_theme_version: Option<u32>,
74
75    #[serde(rename = "@showObjects", skip_serializing_if = "Option::is_none")]
76    pub show_objects: Option<String>,
77
78    #[serde(rename = "@backupFile", skip_serializing_if = "Option::is_none")]
79    pub backup_file: Option<bool>,
80
81    #[serde(rename = "@codeName", skip_serializing_if = "Option::is_none")]
82    pub code_name: Option<String>,
83
84    #[serde(
85        rename = "@checkCompatibility",
86        skip_serializing_if = "Option::is_none"
87    )]
88    pub check_compatibility: Option<bool>,
89
90    #[serde(
91        rename = "@autoCompressPictures",
92        skip_serializing_if = "Option::is_none"
93    )]
94    pub auto_compress_pictures: Option<bool>,
95
96    #[serde(
97        rename = "@saveExternalLinkValues",
98        skip_serializing_if = "Option::is_none"
99    )]
100    pub save_external_link_values: Option<bool>,
101
102    #[serde(rename = "@updateLinks", skip_serializing_if = "Option::is_none")]
103    pub update_links: Option<String>,
104
105    #[serde(
106        rename = "@hidePivotFieldList",
107        skip_serializing_if = "Option::is_none"
108    )]
109    pub hide_pivot_field_list: Option<bool>,
110
111    #[serde(
112        rename = "@showPivotChartFilter",
113        skip_serializing_if = "Option::is_none"
114    )]
115    pub show_pivot_chart_filter: Option<bool>,
116
117    #[serde(rename = "@allowRefreshQuery", skip_serializing_if = "Option::is_none")]
118    pub allow_refresh_query: Option<bool>,
119
120    #[serde(rename = "@publishItems", skip_serializing_if = "Option::is_none")]
121    pub publish_items: Option<bool>,
122
123    #[serde(
124        rename = "@showBorderUnselectedTables",
125        skip_serializing_if = "Option::is_none"
126    )]
127    pub show_border_unselected_tables: Option<bool>,
128
129    #[serde(rename = "@promptedSolutions", skip_serializing_if = "Option::is_none")]
130    pub prompted_solutions: Option<bool>,
131
132    #[serde(rename = "@showInkAnnotation", skip_serializing_if = "Option::is_none")]
133    pub show_ink_annotation: Option<bool>,
134}
135
136/// Book views container.
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct BookViews {
139    #[serde(rename = "workbookView")]
140    pub workbook_views: Vec<WorkbookView>,
141}
142
143/// Individual workbook view.
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct WorkbookView {
146    #[serde(rename = "@xWindow", skip_serializing_if = "Option::is_none")]
147    pub x_window: Option<i32>,
148
149    #[serde(rename = "@yWindow", skip_serializing_if = "Option::is_none")]
150    pub y_window: Option<i32>,
151
152    #[serde(rename = "@windowWidth", skip_serializing_if = "Option::is_none")]
153    pub window_width: Option<u32>,
154
155    #[serde(rename = "@windowHeight", skip_serializing_if = "Option::is_none")]
156    pub window_height: Option<u32>,
157
158    #[serde(rename = "@activeTab", skip_serializing_if = "Option::is_none")]
159    pub active_tab: Option<u32>,
160}
161
162/// Sheets container.
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub struct Sheets {
165    #[serde(rename = "sheet")]
166    pub sheets: Vec<SheetEntry>,
167}
168
169/// Individual sheet entry in the workbook.
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171pub struct SheetEntry {
172    #[serde(rename = "@name")]
173    pub name: String,
174
175    #[serde(rename = "@sheetId")]
176    pub sheet_id: u32,
177
178    #[serde(rename = "@state", skip_serializing_if = "Option::is_none")]
179    pub state: Option<String>,
180
181    #[serde(rename = "@r:id", alias = "@id")]
182    pub r_id: String,
183}
184
185/// Defined names container.
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct DefinedNames {
188    #[serde(rename = "definedName", default)]
189    pub defined_names: Vec<DefinedName>,
190}
191
192/// Individual defined name.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct DefinedName {
195    #[serde(rename = "@name")]
196    pub name: String,
197
198    #[serde(rename = "@localSheetId", skip_serializing_if = "Option::is_none")]
199    pub local_sheet_id: Option<u32>,
200
201    #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")]
202    pub comment: Option<String>,
203
204    #[serde(rename = "@hidden", skip_serializing_if = "Option::is_none")]
205    pub hidden: Option<bool>,
206
207    /// The formula/reference value (element text content).
208    #[serde(rename = "$value")]
209    pub value: String,
210}
211
212/// Workbook-level protection settings.
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub struct WorkbookProtection {
215    #[serde(rename = "@workbookPassword", skip_serializing_if = "Option::is_none")]
216    pub workbook_password: Option<String>,
217
218    #[serde(rename = "@lockStructure", skip_serializing_if = "Option::is_none")]
219    pub lock_structure: Option<bool>,
220
221    #[serde(rename = "@lockWindows", skip_serializing_if = "Option::is_none")]
222    pub lock_windows: Option<bool>,
223
224    #[serde(rename = "@revisionsPassword", skip_serializing_if = "Option::is_none")]
225    pub revisions_password: Option<String>,
226
227    #[serde(rename = "@lockRevision", skip_serializing_if = "Option::is_none")]
228    pub lock_revision: Option<bool>,
229}
230
231/// Calculation properties.
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct CalcPr {
234    #[serde(rename = "@calcId", skip_serializing_if = "Option::is_none")]
235    pub calc_id: Option<u32>,
236
237    #[serde(rename = "@calcMode", skip_serializing_if = "Option::is_none")]
238    pub calc_mode: Option<String>,
239
240    #[serde(rename = "@fullCalcOnLoad", skip_serializing_if = "Option::is_none")]
241    pub full_calc_on_load: Option<bool>,
242
243    #[serde(rename = "@refMode", skip_serializing_if = "Option::is_none")]
244    pub ref_mode: Option<String>,
245
246    #[serde(rename = "@iterate", skip_serializing_if = "Option::is_none")]
247    pub iterate: Option<bool>,
248
249    #[serde(rename = "@iterateCount", skip_serializing_if = "Option::is_none")]
250    pub iterate_count: Option<u32>,
251
252    #[serde(rename = "@iterateDelta", skip_serializing_if = "Option::is_none")]
253    pub iterate_delta: Option<f64>,
254
255    #[serde(rename = "@fullPrecision", skip_serializing_if = "Option::is_none")]
256    pub full_precision: Option<bool>,
257
258    #[serde(rename = "@calcCompleted", skip_serializing_if = "Option::is_none")]
259    pub calc_completed: Option<bool>,
260
261    #[serde(rename = "@calcOnSave", skip_serializing_if = "Option::is_none")]
262    pub calc_on_save: Option<bool>,
263
264    #[serde(rename = "@concurrentCalc", skip_serializing_if = "Option::is_none")]
265    pub concurrent_calc: Option<bool>,
266
267    #[serde(
268        rename = "@concurrentManualCount",
269        skip_serializing_if = "Option::is_none"
270    )]
271    pub concurrent_manual_count: Option<u32>,
272
273    #[serde(rename = "@forceFullCalc", skip_serializing_if = "Option::is_none")]
274    pub force_full_calc: Option<bool>,
275}
276
277/// Container for pivot cache references in the workbook.
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct PivotCaches {
280    #[serde(rename = "pivotCache", default)]
281    pub caches: Vec<PivotCacheEntry>,
282}
283
284/// Individual pivot cache entry linking a cache ID to a relationship.
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286pub struct PivotCacheEntry {
287    #[serde(rename = "@cacheId")]
288    pub cache_id: u32,
289
290    #[serde(rename = "@r:id", alias = "@id")]
291    pub r_id: String,
292}
293
294impl Default for WorkbookXml {
295    fn default() -> Self {
296        Self {
297            xmlns: namespaces::SPREADSHEET_ML.to_string(),
298            xmlns_r: namespaces::RELATIONSHIPS.to_string(),
299            file_version: None,
300            workbook_pr: None,
301            workbook_protection: None,
302            book_views: None,
303            sheets: Sheets {
304                sheets: vec![SheetEntry {
305                    name: "Sheet1".to_string(),
306                    sheet_id: 1,
307                    state: None,
308                    r_id: "rId1".to_string(),
309                }],
310            },
311            defined_names: None,
312            calc_pr: None,
313            pivot_caches: None,
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_workbook_default() {
324        let wb = WorkbookXml::default();
325        assert_eq!(wb.xmlns, namespaces::SPREADSHEET_ML);
326        assert_eq!(wb.xmlns_r, namespaces::RELATIONSHIPS);
327        assert_eq!(wb.sheets.sheets.len(), 1);
328        assert_eq!(wb.sheets.sheets[0].name, "Sheet1");
329        assert_eq!(wb.sheets.sheets[0].sheet_id, 1);
330        assert_eq!(wb.sheets.sheets[0].r_id, "rId1");
331        assert!(wb.sheets.sheets[0].state.is_none());
332        assert!(wb.file_version.is_none());
333        assert!(wb.workbook_pr.is_none());
334        assert!(wb.workbook_protection.is_none());
335        assert!(wb.book_views.is_none());
336        assert!(wb.defined_names.is_none());
337        assert!(wb.calc_pr.is_none());
338        assert!(wb.pivot_caches.is_none());
339    }
340
341    #[test]
342    fn test_workbook_roundtrip() {
343        let wb = WorkbookXml::default();
344        let xml = quick_xml::se::to_string(&wb).unwrap();
345        let parsed: WorkbookXml = quick_xml::de::from_str(&xml).unwrap();
346        assert_eq!(wb.xmlns, parsed.xmlns);
347        assert_eq!(wb.xmlns_r, parsed.xmlns_r);
348        assert_eq!(wb.sheets.sheets.len(), parsed.sheets.sheets.len());
349        assert_eq!(wb.sheets.sheets[0].name, parsed.sheets.sheets[0].name);
350        assert_eq!(
351            wb.sheets.sheets[0].sheet_id,
352            parsed.sheets.sheets[0].sheet_id
353        );
354        assert_eq!(wb.sheets.sheets[0].r_id, parsed.sheets.sheets[0].r_id);
355    }
356
357    #[test]
358    fn test_workbook_serialize_structure() {
359        let wb = WorkbookXml::default();
360        let xml = quick_xml::se::to_string(&wb).unwrap();
361        assert!(xml.contains("<workbook"));
362        assert!(xml.contains("<sheets>"));
363        assert!(xml.contains("<sheet "));
364        assert!(xml.contains("name=\"Sheet1\""));
365        assert!(xml.contains("sheetId=\"1\""));
366    }
367
368    #[test]
369    fn test_workbook_optional_fields_not_serialized() {
370        let wb = WorkbookXml::default();
371        let xml = quick_xml::se::to_string(&wb).unwrap();
372        assert!(!xml.contains("fileVersion"));
373        assert!(!xml.contains("workbookPr"));
374        assert!(!xml.contains("workbookProtection"));
375        assert!(!xml.contains("bookViews"));
376        assert!(!xml.contains("definedNames"));
377        assert!(!xml.contains("calcPr"));
378        assert!(!xml.contains("pivotCaches"));
379    }
380
381    #[test]
382    fn test_workbook_with_all_optional_fields() {
383        let wb = WorkbookXml {
384            xmlns: namespaces::SPREADSHEET_ML.to_string(),
385            xmlns_r: namespaces::RELATIONSHIPS.to_string(),
386            file_version: Some(FileVersion {
387                app_name: Some("xl".to_string()),
388                last_edited: Some("7".to_string()),
389                lowest_edited: Some("7".to_string()),
390                rup_build: Some("27425".to_string()),
391            }),
392            workbook_pr: Some(WorkbookPr {
393                date1904: Some(false),
394                filter_privacy: None,
395                default_theme_version: Some(166925),
396                show_objects: None,
397                backup_file: None,
398                code_name: None,
399                check_compatibility: None,
400                auto_compress_pictures: None,
401                save_external_link_values: None,
402                update_links: None,
403                hide_pivot_field_list: None,
404                show_pivot_chart_filter: None,
405                allow_refresh_query: None,
406                publish_items: None,
407                show_border_unselected_tables: None,
408                prompted_solutions: None,
409                show_ink_annotation: None,
410            }),
411            workbook_protection: None,
412            book_views: Some(BookViews {
413                workbook_views: vec![WorkbookView {
414                    x_window: Some(0),
415                    y_window: Some(0),
416                    window_width: Some(28800),
417                    window_height: Some(12210),
418                    active_tab: Some(0),
419                }],
420            }),
421            sheets: Sheets {
422                sheets: vec![SheetEntry {
423                    name: "Sheet1".to_string(),
424                    sheet_id: 1,
425                    state: None,
426                    r_id: "rId1".to_string(),
427                }],
428            },
429            defined_names: None,
430            calc_pr: Some(CalcPr {
431                calc_id: Some(191029),
432                calc_mode: None,
433                full_calc_on_load: None,
434                ref_mode: None,
435                iterate: None,
436                iterate_count: None,
437                iterate_delta: None,
438                full_precision: None,
439                calc_completed: None,
440                calc_on_save: None,
441                concurrent_calc: None,
442                concurrent_manual_count: None,
443                force_full_calc: None,
444            }),
445            pivot_caches: None,
446        };
447
448        let xml = quick_xml::se::to_string(&wb).unwrap();
449        let parsed: WorkbookXml = quick_xml::de::from_str(&xml).unwrap();
450        assert!(parsed.file_version.is_some());
451        assert!(parsed.workbook_pr.is_some());
452        assert!(parsed.book_views.is_some());
453        assert!(parsed.calc_pr.is_some());
454        assert_eq!(
455            parsed.file_version.as_ref().unwrap().app_name,
456            Some("xl".to_string())
457        );
458        assert_eq!(parsed.calc_pr.as_ref().unwrap().calc_id, Some(191029));
459    }
460
461    #[test]
462    fn test_parse_real_excel_workbook() {
463        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
464<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
465  <sheets>
466    <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
467    <sheet name="Sheet2" sheetId="2" r:id="rId2"/>
468  </sheets>
469</workbook>"#;
470
471        let parsed: WorkbookXml = quick_xml::de::from_str(xml).unwrap();
472        assert_eq!(parsed.sheets.sheets.len(), 2);
473        assert_eq!(parsed.sheets.sheets[0].name, "Sheet1");
474        assert_eq!(parsed.sheets.sheets[0].r_id, "rId1");
475        assert_eq!(parsed.sheets.sheets[1].name, "Sheet2");
476        assert_eq!(parsed.sheets.sheets[1].r_id, "rId2");
477    }
478
479    #[test]
480    fn test_multiple_sheets() {
481        let wb = WorkbookXml {
482            sheets: Sheets {
483                sheets: vec![
484                    SheetEntry {
485                        name: "Data".to_string(),
486                        sheet_id: 1,
487                        state: None,
488                        r_id: "rId1".to_string(),
489                    },
490                    SheetEntry {
491                        name: "Summary".to_string(),
492                        sheet_id: 2,
493                        state: None,
494                        r_id: "rId2".to_string(),
495                    },
496                    SheetEntry {
497                        name: "Hidden".to_string(),
498                        sheet_id: 3,
499                        state: Some("hidden".to_string()),
500                        r_id: "rId3".to_string(),
501                    },
502                ],
503            },
504            ..WorkbookXml::default()
505        };
506
507        let xml = quick_xml::se::to_string(&wb).unwrap();
508        let parsed: WorkbookXml = quick_xml::de::from_str(&xml).unwrap();
509        assert_eq!(parsed.sheets.sheets.len(), 3);
510        assert_eq!(parsed.sheets.sheets[2].state, Some("hidden".to_string()));
511    }
512
513    #[test]
514    fn test_sheet_entry_state_not_serialized_when_none() {
515        let entry = SheetEntry {
516            name: "Sheet1".to_string(),
517            sheet_id: 1,
518            state: None,
519            r_id: "rId1".to_string(),
520        };
521        let xml = quick_xml::se::to_string(&entry).unwrap();
522        assert!(!xml.contains("state"));
523    }
524
525    #[test]
526    fn test_extended_workbook_pr_roundtrip() {
527        let pr = WorkbookPr {
528            date1904: Some(false),
529            filter_privacy: Some(true),
530            default_theme_version: Some(166925),
531            show_objects: Some("all".to_string()),
532            backup_file: Some(true),
533            code_name: Some("ThisWorkbook".to_string()),
534            check_compatibility: Some(true),
535            auto_compress_pictures: Some(false),
536            save_external_link_values: Some(true),
537            update_links: Some("always".to_string()),
538            hide_pivot_field_list: Some(false),
539            show_pivot_chart_filter: Some(true),
540            allow_refresh_query: Some(true),
541            publish_items: Some(false),
542            show_border_unselected_tables: Some(true),
543            prompted_solutions: Some(false),
544            show_ink_annotation: Some(true),
545        };
546        let xml = quick_xml::se::to_string(&pr).unwrap();
547        let parsed: WorkbookPr = quick_xml::de::from_str(&xml).unwrap();
548        assert_eq!(pr, parsed);
549        assert!(xml.contains("showObjects=\"all\""));
550        assert!(xml.contains("backupFile=\"true\""));
551        assert!(xml.contains("codeName=\"ThisWorkbook\""));
552        assert!(xml.contains("checkCompatibility=\"true\""));
553        assert!(xml.contains("autoCompressPictures=\"false\""));
554        assert!(xml.contains("saveExternalLinkValues=\"true\""));
555        assert!(xml.contains("updateLinks=\"always\""));
556        assert!(xml.contains("hidePivotFieldList=\"false\""));
557        assert!(xml.contains("showPivotChartFilter=\"true\""));
558        assert!(xml.contains("allowRefreshQuery=\"true\""));
559        assert!(xml.contains("publishItems=\"false\""));
560        assert!(xml.contains("showBorderUnselectedTables=\"true\""));
561        assert!(xml.contains("promptedSolutions=\"false\""));
562        assert!(xml.contains("showInkAnnotation=\"true\""));
563    }
564
565    #[test]
566    fn test_extended_calc_pr_roundtrip() {
567        let calc = CalcPr {
568            calc_id: Some(191029),
569            calc_mode: Some("auto".to_string()),
570            full_calc_on_load: Some(true),
571            ref_mode: Some("A1".to_string()),
572            iterate: Some(true),
573            iterate_count: Some(100),
574            iterate_delta: Some(0.001),
575            full_precision: Some(true),
576            calc_completed: Some(true),
577            calc_on_save: Some(true),
578            concurrent_calc: Some(true),
579            concurrent_manual_count: Some(4),
580            force_full_calc: Some(false),
581        };
582        let xml = quick_xml::se::to_string(&calc).unwrap();
583        let parsed: CalcPr = quick_xml::de::from_str(&xml).unwrap();
584        assert_eq!(calc, parsed);
585        assert!(xml.contains("refMode=\"A1\""));
586        assert!(xml.contains("iterate=\"true\""));
587        assert!(xml.contains("iterateCount=\"100\""));
588        assert!(xml.contains("iterateDelta=\"0.001\""));
589        assert!(xml.contains("fullPrecision=\"true\""));
590        assert!(xml.contains("calcCompleted=\"true\""));
591        assert!(xml.contains("calcOnSave=\"true\""));
592        assert!(xml.contains("concurrentCalc=\"true\""));
593        assert!(xml.contains("concurrentManualCount=\"4\""));
594        assert!(xml.contains("forceFullCalc=\"false\""));
595    }
596
597    #[test]
598    fn test_workbook_protection_roundtrip() {
599        let prot = WorkbookProtection {
600            workbook_password: Some("ABCD".to_string()),
601            lock_structure: Some(true),
602            lock_windows: Some(false),
603            revisions_password: Some("1234".to_string()),
604            lock_revision: Some(true),
605        };
606        let xml = quick_xml::se::to_string(&prot).unwrap();
607        let parsed: WorkbookProtection = quick_xml::de::from_str(&xml).unwrap();
608        assert_eq!(prot, parsed);
609        assert!(xml.contains("workbookPassword=\"ABCD\""));
610        assert!(xml.contains("lockStructure=\"true\""));
611        assert!(xml.contains("lockWindows=\"false\""));
612        assert!(xml.contains("revisionsPassword=\"1234\""));
613        assert!(xml.contains("lockRevision=\"true\""));
614    }
615
616    #[test]
617    fn test_workbook_protection_optional_fields_skipped() {
618        let prot = WorkbookProtection {
619            workbook_password: None,
620            lock_structure: Some(true),
621            lock_windows: None,
622            revisions_password: None,
623            lock_revision: None,
624        };
625        let xml = quick_xml::se::to_string(&prot).unwrap();
626        assert!(!xml.contains("workbookPassword"));
627        assert!(xml.contains("lockStructure=\"true\""));
628        assert!(!xml.contains("lockWindows"));
629        assert!(!xml.contains("revisionsPassword"));
630        assert!(!xml.contains("lockRevision"));
631    }
632
633    #[test]
634    fn test_workbook_xml_with_protection_roundtrip() {
635        let wb = WorkbookXml {
636            workbook_protection: Some(WorkbookProtection {
637                workbook_password: Some("CC23".to_string()),
638                lock_structure: Some(true),
639                lock_windows: None,
640                revisions_password: None,
641                lock_revision: None,
642            }),
643            ..WorkbookXml::default()
644        };
645        let xml = quick_xml::se::to_string(&wb).unwrap();
646        let parsed: WorkbookXml = quick_xml::de::from_str(&xml).unwrap();
647        assert!(parsed.workbook_protection.is_some());
648        let prot = parsed.workbook_protection.unwrap();
649        assert_eq!(prot.workbook_password, Some("CC23".to_string()));
650        assert_eq!(prot.lock_structure, Some(true));
651    }
652
653    #[test]
654    fn test_workbook_xml_element_order() {
655        let wb = WorkbookXml {
656            workbook_pr: Some(WorkbookPr {
657                date1904: Some(false),
658                filter_privacy: None,
659                default_theme_version: None,
660                show_objects: None,
661                backup_file: None,
662                code_name: None,
663                check_compatibility: None,
664                auto_compress_pictures: None,
665                save_external_link_values: None,
666                update_links: None,
667                hide_pivot_field_list: None,
668                show_pivot_chart_filter: None,
669                allow_refresh_query: None,
670                publish_items: None,
671                show_border_unselected_tables: None,
672                prompted_solutions: None,
673                show_ink_annotation: None,
674            }),
675            workbook_protection: Some(WorkbookProtection {
676                workbook_password: None,
677                lock_structure: Some(true),
678                lock_windows: None,
679                revisions_password: None,
680                lock_revision: None,
681            }),
682            book_views: Some(BookViews {
683                workbook_views: vec![WorkbookView {
684                    x_window: Some(0),
685                    y_window: Some(0),
686                    window_width: Some(28800),
687                    window_height: Some(12210),
688                    active_tab: None,
689                }],
690            }),
691            ..WorkbookXml::default()
692        };
693        let xml = quick_xml::se::to_string(&wb).unwrap();
694        let pr_pos = xml
695            .find("workbookPr")
696            .expect("workbookPr should be present");
697        let prot_pos = xml
698            .find("workbookProtection")
699            .expect("workbookProtection should be present");
700        let bv_pos = xml.find("bookViews").expect("bookViews should be present");
701        assert!(
702            pr_pos < prot_pos,
703            "workbookPr ({pr_pos}) should come before workbookProtection ({prot_pos})"
704        );
705        assert!(
706            prot_pos < bv_pos,
707            "workbookProtection ({prot_pos}) should come before bookViews ({bv_pos})"
708        );
709    }
710}