Skip to main content

sheetkit_xml/
relationships.rs

1//! Relationships XML schema structures.
2//!
3//! Used in `_rels/.rels`, `xl/_rels/workbook.xml.rels`, and other relationship files.
4
5use serde::{Deserialize, Serialize};
6
7use crate::namespaces;
8
9/// Relationships root element.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(rename = "Relationships")]
12pub struct Relationships {
13    #[serde(rename = "@xmlns")]
14    pub xmlns: String,
15
16    #[serde(rename = "Relationship", default)]
17    pub relationships: Vec<Relationship>,
18}
19
20/// Individual relationship entry.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct Relationship {
23    #[serde(rename = "@Id")]
24    pub id: String,
25
26    #[serde(rename = "@Type")]
27    pub rel_type: String,
28
29    #[serde(rename = "@Target")]
30    pub target: String,
31
32    #[serde(rename = "@TargetMode", skip_serializing_if = "Option::is_none")]
33    pub target_mode: Option<String>,
34}
35
36/// Creates the package-level relationships (`_rels/.rels`).
37///
38/// Contains relationships from the package root to the workbook, core
39/// properties, and extended properties parts.
40pub fn package_rels() -> Relationships {
41    Relationships {
42        xmlns: namespaces::PACKAGE_RELATIONSHIPS.to_string(),
43        relationships: vec![
44            Relationship {
45                id: "rId1".to_string(),
46                rel_type: rel_types::OFFICE_DOCUMENT.to_string(),
47                target: "xl/workbook.xml".to_string(),
48                target_mode: None,
49            },
50            Relationship {
51                id: "rId2".to_string(),
52                rel_type: rel_types::CORE_PROPERTIES.to_string(),
53                target: "docProps/core.xml".to_string(),
54                target_mode: None,
55            },
56            Relationship {
57                id: "rId3".to_string(),
58                rel_type: rel_types::EXTENDED_PROPERTIES.to_string(),
59                target: "docProps/app.xml".to_string(),
60                target_mode: None,
61            },
62        ],
63    }
64}
65
66/// Creates the workbook-level relationships (`xl/_rels/workbook.xml.rels`).
67///
68/// Contains relationships to worksheets, styles, and shared strings.
69pub fn workbook_rels() -> Relationships {
70    Relationships {
71        xmlns: namespaces::PACKAGE_RELATIONSHIPS.to_string(),
72        relationships: vec![
73            Relationship {
74                id: "rId1".to_string(),
75                rel_type: rel_types::WORKSHEET.to_string(),
76                target: "worksheets/sheet1.xml".to_string(),
77                target_mode: None,
78            },
79            Relationship {
80                id: "rId2".to_string(),
81                rel_type: rel_types::STYLES.to_string(),
82                target: "styles.xml".to_string(),
83                target_mode: None,
84            },
85            Relationship {
86                id: "rId3".to_string(),
87                rel_type: rel_types::SHARED_STRINGS.to_string(),
88                target: "sharedStrings.xml".to_string(),
89                target_mode: None,
90            },
91        ],
92    }
93}
94
95/// Relationship type URI constants.
96pub mod rel_types {
97    // Package level
98    pub const OFFICE_DOCUMENT: &str =
99        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
100    pub const CORE_PROPERTIES: &str =
101        "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
102    pub const EXTENDED_PROPERTIES: &str =
103        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
104
105    // Workbook level
106    pub const WORKSHEET: &str =
107        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";
108    pub const SHARED_STRINGS: &str =
109        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
110    pub const STYLES: &str =
111        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
112    pub const THEME: &str =
113        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
114    pub const CHARTSHEET: &str =
115        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet";
116    pub const CALC_CHAIN: &str =
117        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain";
118    pub const EXTERNAL_LINK: &str =
119        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink";
120    pub const PIVOT_CACHE_DEF: &str =
121        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition";
122    pub const PIVOT_TABLE: &str =
123        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable";
124    pub const PIVOT_CACHE_RECORDS: &str =
125        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords";
126
127    // Worksheet level
128    pub const COMMENTS: &str =
129        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
130    pub const DRAWING: &str =
131        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";
132    pub const TABLE: &str =
133        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table";
134    pub const HYPERLINK: &str =
135        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
136    pub const PRINTER_SETTINGS: &str =
137        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings";
138
139    pub const VML_DRAWING: &str =
140        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing";
141
142    // Drawing level
143    pub const CHART: &str =
144        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
145    pub const IMAGE: &str =
146        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
147
148    // Custom properties
149    pub const CUSTOM_PROPERTIES: &str =
150        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
151
152    // Slicers
153    pub const SLICER: &str = "http://schemas.microsoft.com/office/2007/relationships/slicer";
154    pub const SLICER_CACHE: &str =
155        "http://schemas.microsoft.com/office/2007/relationships/slicerCache";
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_package_rels_factory() {
164        let rels = package_rels();
165        assert_eq!(rels.xmlns, namespaces::PACKAGE_RELATIONSHIPS);
166        assert_eq!(rels.relationships.len(), 3);
167
168        // Office document relationship
169        assert_eq!(rels.relationships[0].id, "rId1");
170        assert_eq!(rels.relationships[0].rel_type, rel_types::OFFICE_DOCUMENT);
171        assert_eq!(rels.relationships[0].target, "xl/workbook.xml");
172        assert!(rels.relationships[0].target_mode.is_none());
173
174        // Core properties relationship
175        assert_eq!(rels.relationships[1].id, "rId2");
176        assert_eq!(rels.relationships[1].rel_type, rel_types::CORE_PROPERTIES);
177        assert_eq!(rels.relationships[1].target, "docProps/core.xml");
178
179        // Extended properties relationship
180        assert_eq!(rels.relationships[2].id, "rId3");
181        assert_eq!(
182            rels.relationships[2].rel_type,
183            rel_types::EXTENDED_PROPERTIES
184        );
185        assert_eq!(rels.relationships[2].target, "docProps/app.xml");
186    }
187
188    #[test]
189    fn test_workbook_rels_factory() {
190        let rels = workbook_rels();
191        assert_eq!(rels.xmlns, namespaces::PACKAGE_RELATIONSHIPS);
192        assert_eq!(rels.relationships.len(), 3);
193
194        // Verify worksheet relationship
195        assert_eq!(rels.relationships[0].id, "rId1");
196        assert_eq!(rels.relationships[0].rel_type, rel_types::WORKSHEET);
197        assert_eq!(rels.relationships[0].target, "worksheets/sheet1.xml");
198
199        // Verify styles relationship
200        assert_eq!(rels.relationships[1].id, "rId2");
201        assert_eq!(rels.relationships[1].rel_type, rel_types::STYLES);
202        assert_eq!(rels.relationships[1].target, "styles.xml");
203
204        // Verify shared strings relationship
205        assert_eq!(rels.relationships[2].id, "rId3");
206        assert_eq!(rels.relationships[2].rel_type, rel_types::SHARED_STRINGS);
207        assert_eq!(rels.relationships[2].target, "sharedStrings.xml");
208    }
209
210    #[test]
211    fn test_relationships_roundtrip() {
212        let rels = package_rels();
213        let xml = quick_xml::se::to_string(&rels).unwrap();
214        let parsed: Relationships = quick_xml::de::from_str(&xml).unwrap();
215        assert_eq!(rels.xmlns, parsed.xmlns);
216        assert_eq!(rels.relationships.len(), parsed.relationships.len());
217        assert_eq!(rels.relationships[0].id, parsed.relationships[0].id);
218        assert_eq!(
219            rels.relationships[0].rel_type,
220            parsed.relationships[0].rel_type
221        );
222        assert_eq!(rels.relationships[0].target, parsed.relationships[0].target);
223    }
224
225    #[test]
226    fn test_workbook_rels_roundtrip() {
227        let rels = workbook_rels();
228        let xml = quick_xml::se::to_string(&rels).unwrap();
229        let parsed: Relationships = quick_xml::de::from_str(&xml).unwrap();
230        assert_eq!(rels.relationships.len(), parsed.relationships.len());
231        for (orig, parsed) in rels.relationships.iter().zip(parsed.relationships.iter()) {
232            assert_eq!(orig.id, parsed.id);
233            assert_eq!(orig.rel_type, parsed.rel_type);
234            assert_eq!(orig.target, parsed.target);
235        }
236    }
237
238    #[test]
239    fn test_relationship_with_target_mode() {
240        let rel = Relationship {
241            id: "rId1".to_string(),
242            rel_type: rel_types::HYPERLINK.to_string(),
243            target: "https://example.com".to_string(),
244            target_mode: Some("External".to_string()),
245        };
246        let xml = quick_xml::se::to_string(&rel).unwrap();
247        assert!(xml.contains("TargetMode=\"External\""));
248
249        let parsed: Relationship = quick_xml::de::from_str(&xml).unwrap();
250        assert_eq!(parsed.target_mode, Some("External".to_string()));
251    }
252
253    #[test]
254    fn test_relationship_without_target_mode_omits_attr() {
255        let rel = Relationship {
256            id: "rId1".to_string(),
257            rel_type: rel_types::WORKSHEET.to_string(),
258            target: "worksheets/sheet1.xml".to_string(),
259            target_mode: None,
260        };
261        let xml = quick_xml::se::to_string(&rel).unwrap();
262        assert!(!xml.contains("TargetMode"));
263    }
264
265    #[test]
266    fn test_parse_real_excel_rels() {
267        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
268<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
269  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
270</Relationships>"#;
271
272        let parsed: Relationships = quick_xml::de::from_str(xml).unwrap();
273        assert_eq!(parsed.xmlns, namespaces::PACKAGE_RELATIONSHIPS);
274        assert_eq!(parsed.relationships.len(), 1);
275        assert_eq!(parsed.relationships[0].id, "rId1");
276        assert_eq!(parsed.relationships[0].target, "xl/workbook.xml");
277    }
278
279    #[test]
280    fn test_parse_real_excel_workbook_rels() {
281        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
282<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
283  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
284  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
285  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>
286</Relationships>"#;
287
288        let parsed: Relationships = quick_xml::de::from_str(xml).unwrap();
289        assert_eq!(parsed.relationships.len(), 3);
290        assert_eq!(parsed.relationships[0].rel_type, rel_types::WORKSHEET);
291        assert_eq!(parsed.relationships[1].rel_type, rel_types::STYLES);
292        assert_eq!(parsed.relationships[2].rel_type, rel_types::SHARED_STRINGS);
293    }
294
295    #[test]
296    fn test_serialize_structure() {
297        let rels = package_rels();
298        let xml = quick_xml::se::to_string(&rels).unwrap();
299        assert!(xml.contains("<Relationships"));
300        assert!(xml.contains("<Relationship"));
301        assert!(xml.contains("Id="));
302        assert!(xml.contains("Type="));
303        assert!(xml.contains("Target="));
304    }
305
306    #[test]
307    fn test_empty_relationships() {
308        let rels = Relationships {
309            xmlns: namespaces::PACKAGE_RELATIONSHIPS.to_string(),
310            relationships: vec![],
311        };
312        let xml = quick_xml::se::to_string(&rels).unwrap();
313        let parsed: Relationships = quick_xml::de::from_str(&xml).unwrap();
314        assert!(parsed.relationships.is_empty());
315    }
316}