Skip to main content

sheetkit_xml/
pivot_cache.rs

1//! Pivot cache XML schema structures.
2//!
3//! Represents `xl/pivotCache/pivotCacheDefinition{N}.xml` and
4//! `xl/pivotCache/pivotCacheRecords{N}.xml`.
5
6use serde::{Deserialize, Serialize};
7
8/// Root element for pivot cache definition.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(rename = "pivotCacheDefinition")]
11pub struct PivotCacheDefinition {
12    #[serde(rename = "@xmlns")]
13    pub xmlns: String,
14
15    #[serde(rename = "@xmlns:r")]
16    pub xmlns_r: String,
17
18    #[serde(
19        rename = "@r:id",
20        alias = "@id",
21        skip_serializing_if = "Option::is_none"
22    )]
23    pub r_id: Option<String>,
24
25    #[serde(rename = "@recordCount", skip_serializing_if = "Option::is_none")]
26    pub record_count: Option<u32>,
27
28    #[serde(rename = "cacheSource")]
29    pub cache_source: CacheSource,
30
31    #[serde(rename = "cacheFields")]
32    pub cache_fields: CacheFields,
33}
34
35/// Source of data for the pivot cache.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct CacheSource {
38    #[serde(rename = "@type")]
39    pub source_type: String,
40
41    #[serde(rename = "worksheetSource", skip_serializing_if = "Option::is_none")]
42    pub worksheet_source: Option<WorksheetSource>,
43}
44
45/// Worksheet-based data source.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct WorksheetSource {
48    #[serde(rename = "@ref")]
49    pub reference: String,
50
51    #[serde(rename = "@sheet")]
52    pub sheet: String,
53}
54
55/// Container for cache field definitions.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct CacheFields {
58    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
59    pub count: Option<u32>,
60
61    #[serde(rename = "cacheField", default)]
62    pub fields: Vec<CacheField>,
63}
64
65/// Individual cache field definition.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct CacheField {
68    #[serde(rename = "@name")]
69    pub name: String,
70
71    #[serde(rename = "@numFmtId", skip_serializing_if = "Option::is_none")]
72    pub num_fmt_id: Option<u32>,
73
74    #[serde(rename = "sharedItems", skip_serializing_if = "Option::is_none")]
75    pub shared_items: Option<SharedItems>,
76}
77
78/// Shared items within a cache field.
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct SharedItems {
81    #[serde(
82        rename = "@containsSemiMixedTypes",
83        skip_serializing_if = "Option::is_none"
84    )]
85    pub contains_semi_mixed_types: Option<bool>,
86
87    #[serde(rename = "@containsString", skip_serializing_if = "Option::is_none")]
88    pub contains_string: Option<bool>,
89
90    #[serde(rename = "@containsNumber", skip_serializing_if = "Option::is_none")]
91    pub contains_number: Option<bool>,
92
93    #[serde(rename = "@containsBlank", skip_serializing_if = "Option::is_none")]
94    pub contains_blank: Option<bool>,
95
96    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
97    pub count: Option<u32>,
98
99    #[serde(rename = "s", default)]
100    pub string_items: Vec<StringItem>,
101
102    #[serde(rename = "n", default)]
103    pub number_items: Vec<NumberItem>,
104}
105
106/// String value in shared items.
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub struct StringItem {
109    #[serde(rename = "@v")]
110    pub value: String,
111}
112
113/// Numeric value in shared items.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct NumberItem {
116    #[serde(rename = "@v")]
117    pub value: f64,
118}
119
120/// Root element for pivot cache records.
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122#[serde(rename = "pivotCacheRecords")]
123pub struct PivotCacheRecords {
124    #[serde(rename = "@xmlns")]
125    pub xmlns: String,
126
127    #[serde(rename = "@xmlns:r")]
128    pub xmlns_r: String,
129
130    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
131    pub count: Option<u32>,
132
133    #[serde(rename = "r", default)]
134    pub records: Vec<CacheRecord>,
135}
136
137/// Individual cache record containing field values.
138///
139/// Each record uses separate optional vectors for each value type because
140/// serde + quick-xml does not reliably roundtrip internally-tagged enums
141/// within a mixed-content element. This flat representation is simpler and
142/// still captures the data faithfully.
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct CacheRecord {
145    #[serde(rename = "x", default)]
146    pub index_fields: Vec<IndexField>,
147
148    #[serde(rename = "n", default)]
149    pub number_fields: Vec<NumberField>,
150
151    #[serde(rename = "s", default)]
152    pub string_fields: Vec<StringField>,
153
154    #[serde(rename = "b", default)]
155    pub bool_fields: Vec<BoolField>,
156}
157
158/// Index reference field in a cache record.
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct IndexField {
161    #[serde(rename = "@v")]
162    pub v: u32,
163}
164
165/// Number field in a cache record.
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct NumberField {
168    #[serde(rename = "@v")]
169    pub v: f64,
170}
171
172/// String field in a cache record.
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174pub struct StringField {
175    #[serde(rename = "@v")]
176    pub v: String,
177}
178
179/// Boolean field in a cache record.
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub struct BoolField {
182    #[serde(rename = "@v")]
183    pub v: bool,
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_worksheet_source_roundtrip() {
192        let src = WorksheetSource {
193            reference: "A1:D10".to_string(),
194            sheet: "Sheet1".to_string(),
195        };
196        let xml = quick_xml::se::to_string(&src).unwrap();
197        let parsed: WorksheetSource = quick_xml::de::from_str(&xml).unwrap();
198        assert_eq!(src, parsed);
199    }
200
201    #[test]
202    fn test_cache_source_roundtrip() {
203        let src = CacheSource {
204            source_type: "worksheet".to_string(),
205            worksheet_source: Some(WorksheetSource {
206                reference: "A1:D10".to_string(),
207                sheet: "Data".to_string(),
208            }),
209        };
210        let xml = quick_xml::se::to_string(&src).unwrap();
211        let parsed: CacheSource = quick_xml::de::from_str(&xml).unwrap();
212        assert_eq!(src, parsed);
213    }
214
215    #[test]
216    fn test_cache_source_without_worksheet() {
217        let src = CacheSource {
218            source_type: "external".to_string(),
219            worksheet_source: None,
220        };
221        let xml = quick_xml::se::to_string(&src).unwrap();
222        assert!(!xml.contains("worksheetSource"));
223        let parsed: CacheSource = quick_xml::de::from_str(&xml).unwrap();
224        assert_eq!(src, parsed);
225    }
226
227    #[test]
228    fn test_string_item_roundtrip() {
229        let item = StringItem {
230            value: "North".to_string(),
231        };
232        let xml = quick_xml::se::to_string(&item).unwrap();
233        let parsed: StringItem = quick_xml::de::from_str(&xml).unwrap();
234        assert_eq!(item, parsed);
235    }
236
237    #[test]
238    fn test_number_item_roundtrip() {
239        let item = NumberItem { value: 42.5 };
240        let xml = quick_xml::se::to_string(&item).unwrap();
241        let parsed: NumberItem = quick_xml::de::from_str(&xml).unwrap();
242        assert_eq!(item, parsed);
243    }
244
245    #[test]
246    fn test_shared_items_with_strings_roundtrip() {
247        let items = SharedItems {
248            contains_semi_mixed_types: Some(false),
249            contains_string: Some(true),
250            contains_number: Some(false),
251            contains_blank: None,
252            count: Some(3),
253            string_items: vec![
254                StringItem {
255                    value: "North".to_string(),
256                },
257                StringItem {
258                    value: "South".to_string(),
259                },
260                StringItem {
261                    value: "East".to_string(),
262                },
263            ],
264            number_items: vec![],
265        };
266        let xml = quick_xml::se::to_string(&items).unwrap();
267        let parsed: SharedItems = quick_xml::de::from_str(&xml).unwrap();
268        assert_eq!(items, parsed);
269    }
270
271    #[test]
272    fn test_shared_items_with_numbers_roundtrip() {
273        let items = SharedItems {
274            contains_semi_mixed_types: None,
275            contains_string: Some(false),
276            contains_number: Some(true),
277            contains_blank: None,
278            count: Some(2),
279            string_items: vec![],
280            number_items: vec![NumberItem { value: 100.0 }, NumberItem { value: 200.0 }],
281        };
282        let xml = quick_xml::se::to_string(&items).unwrap();
283        let parsed: SharedItems = quick_xml::de::from_str(&xml).unwrap();
284        assert_eq!(items, parsed);
285    }
286
287    #[test]
288    fn test_shared_items_empty_roundtrip() {
289        let items = SharedItems {
290            contains_semi_mixed_types: None,
291            contains_string: None,
292            contains_number: None,
293            contains_blank: None,
294            count: Some(0),
295            string_items: vec![],
296            number_items: vec![],
297        };
298        let xml = quick_xml::se::to_string(&items).unwrap();
299        let parsed: SharedItems = quick_xml::de::from_str(&xml).unwrap();
300        assert_eq!(items, parsed);
301    }
302
303    #[test]
304    fn test_cache_field_roundtrip() {
305        let field = CacheField {
306            name: "Region".to_string(),
307            num_fmt_id: Some(0),
308            shared_items: Some(SharedItems {
309                contains_semi_mixed_types: None,
310                contains_string: Some(true),
311                contains_number: None,
312                contains_blank: None,
313                count: Some(2),
314                string_items: vec![
315                    StringItem {
316                        value: "North".to_string(),
317                    },
318                    StringItem {
319                        value: "South".to_string(),
320                    },
321                ],
322                number_items: vec![],
323            }),
324        };
325        let xml = quick_xml::se::to_string(&field).unwrap();
326        let parsed: CacheField = quick_xml::de::from_str(&xml).unwrap();
327        assert_eq!(field, parsed);
328    }
329
330    #[test]
331    fn test_cache_field_no_shared_items() {
332        let field = CacheField {
333            name: "Amount".to_string(),
334            num_fmt_id: None,
335            shared_items: None,
336        };
337        let xml = quick_xml::se::to_string(&field).unwrap();
338        assert!(!xml.contains("sharedItems"));
339        assert!(!xml.contains("numFmtId"));
340        let parsed: CacheField = quick_xml::de::from_str(&xml).unwrap();
341        assert_eq!(field, parsed);
342    }
343
344    #[test]
345    fn test_cache_fields_roundtrip() {
346        let fields = CacheFields {
347            count: Some(2),
348            fields: vec![
349                CacheField {
350                    name: "Region".to_string(),
351                    num_fmt_id: Some(0),
352                    shared_items: None,
353                },
354                CacheField {
355                    name: "Sales".to_string(),
356                    num_fmt_id: Some(0),
357                    shared_items: None,
358                },
359            ],
360        };
361        let xml = quick_xml::se::to_string(&fields).unwrap();
362        let parsed: CacheFields = quick_xml::de::from_str(&xml).unwrap();
363        assert_eq!(fields, parsed);
364    }
365
366    #[test]
367    fn test_pivot_cache_definition_roundtrip() {
368        let def = PivotCacheDefinition {
369            xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main".to_string(),
370            xmlns_r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
371                .to_string(),
372            r_id: None,
373            record_count: Some(5),
374            cache_source: CacheSource {
375                source_type: "worksheet".to_string(),
376                worksheet_source: Some(WorksheetSource {
377                    reference: "A1:C6".to_string(),
378                    sheet: "Data".to_string(),
379                }),
380            },
381            cache_fields: CacheFields {
382                count: Some(3),
383                fields: vec![
384                    CacheField {
385                        name: "Name".to_string(),
386                        num_fmt_id: Some(0),
387                        shared_items: None,
388                    },
389                    CacheField {
390                        name: "Region".to_string(),
391                        num_fmt_id: Some(0),
392                        shared_items: None,
393                    },
394                    CacheField {
395                        name: "Sales".to_string(),
396                        num_fmt_id: Some(0),
397                        shared_items: None,
398                    },
399                ],
400            },
401        };
402        let xml = quick_xml::se::to_string(&def).unwrap();
403        let parsed: PivotCacheDefinition = quick_xml::de::from_str(&xml).unwrap();
404        assert_eq!(def, parsed);
405    }
406
407    #[test]
408    fn test_pivot_cache_definition_structure() {
409        let def = PivotCacheDefinition {
410            xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main".to_string(),
411            xmlns_r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
412                .to_string(),
413            r_id: Some("rId1".to_string()),
414            record_count: Some(10),
415            cache_source: CacheSource {
416                source_type: "worksheet".to_string(),
417                worksheet_source: Some(WorksheetSource {
418                    reference: "A1:D11".to_string(),
419                    sheet: "Sheet1".to_string(),
420                }),
421            },
422            cache_fields: CacheFields {
423                count: Some(1),
424                fields: vec![CacheField {
425                    name: "Col1".to_string(),
426                    num_fmt_id: None,
427                    shared_items: None,
428                }],
429            },
430        };
431        let xml = quick_xml::se::to_string(&def).unwrap();
432        assert!(xml.contains("<pivotCacheDefinition"));
433        assert!(xml.contains("recordCount=\"10\""));
434        assert!(xml.contains("<cacheSource"));
435        assert!(xml.contains("type=\"worksheet\""));
436        assert!(xml.contains("<worksheetSource"));
437        assert!(xml.contains("<cacheFields"));
438    }
439
440    #[test]
441    fn test_index_field_roundtrip() {
442        let field = IndexField { v: 3 };
443        let xml = quick_xml::se::to_string(&field).unwrap();
444        let parsed: IndexField = quick_xml::de::from_str(&xml).unwrap();
445        assert_eq!(field, parsed);
446    }
447
448    #[test]
449    fn test_number_field_roundtrip() {
450        let field = NumberField { v: 99.5 };
451        let xml = quick_xml::se::to_string(&field).unwrap();
452        let parsed: NumberField = quick_xml::de::from_str(&xml).unwrap();
453        assert_eq!(field, parsed);
454    }
455
456    #[test]
457    fn test_string_field_roundtrip() {
458        let field = StringField {
459            v: "hello".to_string(),
460        };
461        let xml = quick_xml::se::to_string(&field).unwrap();
462        let parsed: StringField = quick_xml::de::from_str(&xml).unwrap();
463        assert_eq!(field, parsed);
464    }
465
466    #[test]
467    fn test_bool_field_roundtrip() {
468        let field = BoolField { v: true };
469        let xml = quick_xml::se::to_string(&field).unwrap();
470        let parsed: BoolField = quick_xml::de::from_str(&xml).unwrap();
471        assert_eq!(field, parsed);
472    }
473
474    #[test]
475    fn test_cache_record_roundtrip() {
476        let record = CacheRecord {
477            index_fields: vec![IndexField { v: 0 }, IndexField { v: 1 }],
478            number_fields: vec![NumberField { v: 150.0 }],
479            string_fields: vec![],
480            bool_fields: vec![],
481        };
482        let xml = quick_xml::se::to_string(&record).unwrap();
483        let parsed: CacheRecord = quick_xml::de::from_str(&xml).unwrap();
484        assert_eq!(record, parsed);
485    }
486
487    #[test]
488    fn test_cache_record_with_strings() {
489        let record = CacheRecord {
490            index_fields: vec![],
491            number_fields: vec![],
492            string_fields: vec![
493                StringField {
494                    v: "alpha".to_string(),
495                },
496                StringField {
497                    v: "beta".to_string(),
498                },
499            ],
500            bool_fields: vec![BoolField { v: false }],
501        };
502        let xml = quick_xml::se::to_string(&record).unwrap();
503        let parsed: CacheRecord = quick_xml::de::from_str(&xml).unwrap();
504        assert_eq!(record, parsed);
505    }
506
507    #[test]
508    fn test_pivot_cache_records_roundtrip() {
509        let records = PivotCacheRecords {
510            xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main".to_string(),
511            xmlns_r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
512                .to_string(),
513            count: Some(2),
514            records: vec![
515                CacheRecord {
516                    index_fields: vec![IndexField { v: 0 }],
517                    number_fields: vec![NumberField { v: 100.0 }],
518                    string_fields: vec![],
519                    bool_fields: vec![],
520                },
521                CacheRecord {
522                    index_fields: vec![IndexField { v: 1 }],
523                    number_fields: vec![NumberField { v: 200.0 }],
524                    string_fields: vec![],
525                    bool_fields: vec![],
526                },
527            ],
528        };
529        let xml = quick_xml::se::to_string(&records).unwrap();
530        let parsed: PivotCacheRecords = quick_xml::de::from_str(&xml).unwrap();
531        assert_eq!(records, parsed);
532    }
533
534    #[test]
535    fn test_pivot_cache_records_empty() {
536        let records = PivotCacheRecords {
537            xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main".to_string(),
538            xmlns_r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
539                .to_string(),
540            count: Some(0),
541            records: vec![],
542        };
543        let xml = quick_xml::se::to_string(&records).unwrap();
544        let parsed: PivotCacheRecords = quick_xml::de::from_str(&xml).unwrap();
545        assert_eq!(records, parsed);
546    }
547
548    #[test]
549    fn test_pivot_cache_records_structure() {
550        let records = PivotCacheRecords {
551            xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main".to_string(),
552            xmlns_r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
553                .to_string(),
554            count: Some(1),
555            records: vec![CacheRecord {
556                index_fields: vec![IndexField { v: 0 }],
557                number_fields: vec![],
558                string_fields: vec![],
559                bool_fields: vec![],
560            }],
561        };
562        let xml = quick_xml::se::to_string(&records).unwrap();
563        assert!(xml.contains("<pivotCacheRecords"));
564        assert!(xml.contains("count=\"1\""));
565    }
566}