use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename = "multistatus")]
pub struct Multistatus {
#[serde(rename = "response", default)]
pub response: Vec<Response>,
}
impl Multistatus {
pub fn from_xml(xml: &str) -> Result<Self, quick_xml::DeError> {
quick_xml::de::from_str(xml)
}
pub fn to_xml(&self) -> Result<String, quick_xml::SeError> {
quick_xml::se::to_string(self)
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename = "response")]
pub struct Response {
pub href: String,
pub propstat: PropStat,
}
impl Response {
pub fn is_collection(&self) -> bool {
self.propstat
.prop
.resourcetype
.as_ref()
.and_then(|rt| rt.collection.as_ref())
.is_some()
}
pub fn name(&self) -> &str {
self.href
.trim()
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or(&self.href)
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename = "propstat")]
pub struct PropStat {
pub prop: Prop,
pub status: String,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename = "prop")]
pub struct Prop {
#[serde(rename = "displayname", default, skip_serializing_if = "Option::is_none")]
pub displayname: Option<String>,
#[serde(rename = "creationdate", default, skip_serializing_if = "Option::is_none")]
pub creationdate: Option<String>,
#[serde(rename = "getcontentlength", default, skip_serializing_if = "Option::is_none")]
pub getcontentlength: Option<u64>,
#[serde(rename = "getcontenttype", default, skip_serializing_if = "Option::is_none")]
pub getcontenttype: Option<String>,
#[serde(rename = "getetag", default, skip_serializing_if = "Option::is_none")]
pub getetag: Option<String>,
#[serde(rename = "getlastmodified", default, skip_serializing_if = "Option::is_none")]
pub getlastmodified: Option<String>,
#[serde(rename = "lockdiscovery", default, skip_serializing_if = "Option::is_none")]
pub lockdiscovery: Option<String>,
#[serde(rename = "resourcetype", default, skip_serializing_if = "Option::is_none")]
pub resourcetype: Option<ResourceType>,
#[serde(rename = "supportedlock", default, skip_serializing_if = "Option::is_none")]
pub supportedlock: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename = "resourcetype")]
pub struct ResourceType {
#[serde(rename = "collection", default, skip_serializing_if = "Option::is_none")]
pub collection: Option<Collection>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename = "collection")]
pub struct Collection {}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
fn test_roundtrip(xml_content: &str, test_name: &str) {
let multistatus1 = Multistatus::from_xml(xml_content)
.unwrap_or_else(|e| panic!("{}: Failed to parse XML: {}", test_name, e));
let xml_output = multistatus1
.to_xml()
.unwrap_or_else(|e| panic!("{}: Failed to serialize to XML: {}", test_name, e));
let multistatus2 = Multistatus::from_xml(&xml_output)
.unwrap_or_else(|e| panic!("{}: Failed to parse serialized XML: {}", test_name, e));
assert_eq!(
multistatus1, multistatus2,
"{}: Roundtrip conversion failed - objects are not equal",
test_name
);
}
#[test]
fn test_roundtrip_a_xml() {
let xml_path = Path::new("xml/a.xml");
if xml_path.exists() {
let xml_content = fs::read_to_string(xml_path)
.expect("Failed to read xml/a.xml");
test_roundtrip(&xml_content, "a.xml");
} else {
println!("Skipping test_roundtrip_a_xml: file not found");
}
}
#[test]
fn test_roundtrip_dufs_xml() {
let xml_path = Path::new("xml/dufs.xml");
if xml_path.exists() {
let xml_content = fs::read_to_string(xml_path)
.expect("Failed to read xml/dufs.xml");
test_roundtrip(&xml_content, "dufs.xml");
} else {
println!("Skipping test_roundtrip_dufs_xml: file not found");
}
}
#[test]
fn test_roundtrip_list_xml() {
let xml_path = Path::new("xml/list.xml");
if xml_path.exists() {
let xml_content = fs::read_to_string(xml_path)
.expect("Failed to read xml/list.xml");
test_roundtrip(&xml_content, "list.xml");
} else {
println!("Skipping test_roundtrip_list_xml: file not found");
}
}
#[test]
fn test_simple_roundtrip() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<multistatus>
<response>
<href>/test.txt</href>
<propstat>
<prop>
<displayname>test.txt</displayname>
<getcontentlength>1024</getcontentlength>
<getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
<resourcetype></resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
test_roundtrip(xml, "simple");
}
#[test]
fn test_collection_roundtrip() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<multistatus>
<response>
<href>/folder/</href>
<propstat>
<prop>
<displayname>folder</displayname>
<getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
<resourcetype>
<collection></collection>
</resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
test_roundtrip(xml, "collection");
}
#[test]
fn test_is_collection() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<multistatus>
<response>
<href>/folder/</href>
<propstat>
<prop>
<getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
<resourcetype>
<collection></collection>
</resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/file.txt</href>
<propstat>
<prop>
<getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
<resourcetype></resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let multistatus = Multistatus::from_xml(xml).unwrap();
assert_eq!(multistatus.response.len(), 2);
assert!(multistatus.response[0].is_collection());
assert!(!multistatus.response[1].is_collection());
}
#[test]
fn test_name_extraction() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<multistatus>
<response>
<href>/path/to/file.txt</href>
<propstat>
<prop>
<getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
<resourcetype></resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/folder/</href>
<propstat>
<prop>
<getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
<resourcetype></resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let multistatus = Multistatus::from_xml(xml).unwrap();
assert_eq!(multistatus.response[0].name(), "file.txt");
assert_eq!(multistatus.response[1].name(), "folder");
}
}