Skip to main content

webdav_serde/
lib.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Deserialize, Serialize, PartialEq)]
4#[serde(rename = "multistatus")]
5pub struct Multistatus {
6    #[serde(rename = "response", default)]
7    pub response: Vec<Response>,
8}
9
10impl Multistatus {
11    /// Parse WebDAV multistatus XML from a string
12    pub fn from_xml(xml: &str) -> Result<Self, quick_xml::DeError> {
13        quick_xml::de::from_str(xml)
14    }
15
16    /// Serialize to XML string
17    pub fn to_xml(&self) -> Result<String, quick_xml::SeError> {
18        quick_xml::se::to_string(self)
19    }
20}
21
22#[derive(Debug, Deserialize, Serialize, PartialEq)]
23#[serde(rename = "response")]
24pub struct Response {
25    pub href: String,
26    pub propstat: PropStat,
27}
28
29impl Response {
30    /// Check if this response represents a collection (directory)
31    pub fn is_collection(&self) -> bool {
32        self.propstat
33            .prop
34            .resourcetype
35            .as_ref()
36            .and_then(|rt| rt.collection.as_ref())
37            .is_some()
38    }
39
40    /// Get the file/directory name from href
41    pub fn name(&self) -> &str {
42        self.href
43            .trim()
44            .trim_end_matches('/')
45            .rsplit('/')
46            .next()
47            .unwrap_or(&self.href)
48    }
49}
50
51#[derive(Debug, Deserialize, Serialize, PartialEq)]
52#[serde(rename = "propstat")]
53pub struct PropStat {
54    pub prop: Prop,
55    pub status: String,
56}
57
58#[derive(Debug, Deserialize, Serialize, PartialEq)]
59#[serde(rename = "prop")]
60pub struct Prop {
61    #[serde(rename = "displayname", default, skip_serializing_if = "Option::is_none")]
62    pub displayname: Option<String>,
63    
64    #[serde(rename = "creationdate", default, skip_serializing_if = "Option::is_none")]
65    pub creationdate: Option<String>,
66    
67    #[serde(rename = "getcontentlength", default, skip_serializing_if = "Option::is_none")]
68    pub getcontentlength: Option<u64>,
69    
70    #[serde(rename = "getcontenttype", default, skip_serializing_if = "Option::is_none")]
71    pub getcontenttype: Option<String>,
72    
73    #[serde(rename = "getetag", default, skip_serializing_if = "Option::is_none")]
74    pub getetag: Option<String>,
75    
76    #[serde(rename = "getlastmodified", default, skip_serializing_if = "Option::is_none")]
77    pub getlastmodified: Option<String>,
78    
79    #[serde(rename = "lockdiscovery", default, skip_serializing_if = "Option::is_none")]
80    pub lockdiscovery: Option<String>,
81    
82    #[serde(rename = "resourcetype", default, skip_serializing_if = "Option::is_none")]
83    pub resourcetype: Option<ResourceType>,
84    
85    #[serde(rename = "supportedlock", default, skip_serializing_if = "Option::is_none")]
86    pub supportedlock: Option<String>,
87}
88
89#[derive(Debug, Deserialize, Serialize, PartialEq)]
90#[serde(rename = "resourcetype")]
91pub struct ResourceType {
92    #[serde(rename = "collection", default, skip_serializing_if = "Option::is_none")]
93    pub collection: Option<Collection>,
94}
95
96#[derive(Debug, Deserialize, Serialize, PartialEq)]
97#[serde(rename = "collection")]
98pub struct Collection {}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use std::fs;
104    use std::path::Path;
105
106    fn test_roundtrip(xml_content: &str, test_name: &str) {
107        // First deserialization: XML -> Multistatus
108        let multistatus1 = Multistatus::from_xml(xml_content)
109            .unwrap_or_else(|e| panic!("{}: Failed to parse XML: {}", test_name, e));
110
111        // Serialization: Multistatus -> XML
112        let xml_output = multistatus1
113            .to_xml()
114            .unwrap_or_else(|e| panic!("{}: Failed to serialize to XML: {}", test_name, e));
115
116        // Second deserialization: XML -> Multistatus
117        let multistatus2 = Multistatus::from_xml(&xml_output)
118            .unwrap_or_else(|e| panic!("{}: Failed to parse serialized XML: {}", test_name, e));
119
120        // Compare the two Multistatus objects
121        assert_eq!(
122            multistatus1, multistatus2,
123            "{}: Roundtrip conversion failed - objects are not equal",
124            test_name
125        );
126    }
127
128    #[test]
129    fn test_roundtrip_a_xml() {
130        let xml_path = Path::new("xml/a.xml");
131        if xml_path.exists() {
132            let xml_content = fs::read_to_string(xml_path)
133                .expect("Failed to read xml/a.xml");
134            test_roundtrip(&xml_content, "a.xml");
135        } else {
136            println!("Skipping test_roundtrip_a_xml: file not found");
137        }
138    }
139
140    #[test]
141    fn test_roundtrip_dufs_xml() {
142        let xml_path = Path::new("xml/dufs.xml");
143        if xml_path.exists() {
144            let xml_content = fs::read_to_string(xml_path)
145                .expect("Failed to read xml/dufs.xml");
146            test_roundtrip(&xml_content, "dufs.xml");
147        } else {
148            println!("Skipping test_roundtrip_dufs_xml: file not found");
149        }
150    }
151
152    #[test]
153    fn test_roundtrip_list_xml() {
154        let xml_path = Path::new("xml/list.xml");
155        if xml_path.exists() {
156            let xml_content = fs::read_to_string(xml_path)
157                .expect("Failed to read xml/list.xml");
158            test_roundtrip(&xml_content, "list.xml");
159        } else {
160            println!("Skipping test_roundtrip_list_xml: file not found");
161        }
162    }
163
164    #[test]
165    fn test_simple_roundtrip() {
166        let xml = r#"<?xml version="1.0" encoding="utf-8"?>
167<multistatus>
168  <response>
169    <href>/test.txt</href>
170    <propstat>
171      <prop>
172        <displayname>test.txt</displayname>
173        <getcontentlength>1024</getcontentlength>
174        <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
175        <resourcetype></resourcetype>
176      </prop>
177      <status>HTTP/1.1 200 OK</status>
178    </propstat>
179  </response>
180</multistatus>"#;
181
182        test_roundtrip(xml, "simple");
183    }
184
185    #[test]
186    fn test_collection_roundtrip() {
187        let xml = r#"<?xml version="1.0" encoding="utf-8"?>
188<multistatus>
189  <response>
190    <href>/folder/</href>
191    <propstat>
192      <prop>
193        <displayname>folder</displayname>
194        <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
195        <resourcetype>
196          <collection></collection>
197        </resourcetype>
198      </prop>
199      <status>HTTP/1.1 200 OK</status>
200    </propstat>
201  </response>
202</multistatus>"#;
203
204        test_roundtrip(xml, "collection");
205    }
206
207    #[test]
208    fn test_is_collection() {
209        let xml = r#"<?xml version="1.0" encoding="utf-8"?>
210<multistatus>
211  <response>
212    <href>/folder/</href>
213    <propstat>
214      <prop>
215        <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
216        <resourcetype>
217          <collection></collection>
218        </resourcetype>
219      </prop>
220      <status>HTTP/1.1 200 OK</status>
221    </propstat>
222  </response>
223  <response>
224    <href>/file.txt</href>
225    <propstat>
226      <prop>
227        <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
228        <resourcetype></resourcetype>
229      </prop>
230      <status>HTTP/1.1 200 OK</status>
231    </propstat>
232  </response>
233</multistatus>"#;
234
235        let multistatus = Multistatus::from_xml(xml).unwrap();
236        assert_eq!(multistatus.response.len(), 2);
237        assert!(multistatus.response[0].is_collection());
238        assert!(!multistatus.response[1].is_collection());
239    }
240
241    #[test]
242    fn test_name_extraction() {
243        let xml = r#"<?xml version="1.0" encoding="utf-8"?>
244<multistatus>
245  <response>
246    <href>/path/to/file.txt</href>
247    <propstat>
248      <prop>
249        <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
250        <resourcetype></resourcetype>
251      </prop>
252      <status>HTTP/1.1 200 OK</status>
253    </propstat>
254  </response>
255  <response>
256    <href>/folder/</href>
257    <propstat>
258      <prop>
259        <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
260        <resourcetype></resourcetype>
261      </prop>
262      <status>HTTP/1.1 200 OK</status>
263    </propstat>
264  </response>
265</multistatus>"#;
266
267        let multistatus = Multistatus::from_xml(xml).unwrap();
268        assert_eq!(multistatus.response[0].name(), "file.txt");
269        assert_eq!(multistatus.response[1].name(), "folder");
270    }
271}