1use serde::{Deserialize, Serialize};
6
7use crate::namespaces;
8
9#[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#[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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct BookViews {
139 #[serde(rename = "workbookView")]
140 pub workbook_views: Vec<WorkbookView>,
141}
142
143#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub struct Sheets {
165 #[serde(rename = "sheet")]
166 pub sheets: Vec<SheetEntry>,
167}
168
169#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct DefinedNames {
188 #[serde(rename = "definedName", default)]
189 pub defined_names: Vec<DefinedName>,
190}
191
192#[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 #[serde(rename = "$value")]
209 pub value: String,
210}
211
212#[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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct PivotCaches {
280 #[serde(rename = "pivotCache", default)]
281 pub caches: Vec<PivotCacheEntry>,
282}
283
284#[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}