use serde::{Deserialize, Serialize};
use crate::error::{OssError, OssErrorKind, Result};
pub fn to_xml<T: Serialize>(value: &T) -> Result<String> {
quick_xml::se::to_string(value).map_err(|e| OssError {
kind: OssErrorKind::XmlError,
context: Box::new(crate::error::ErrorContext {
operation: Some(format!("serialize {}", std::any::type_name::<T>())),
..Default::default()
}),
source: Some(Box::new(e)),
})
}
pub fn from_xml<T: for<'de> Deserialize<'de>>(xml: &str) -> Result<T> {
quick_xml::de::from_str(xml).map_err(|e| OssError {
kind: OssErrorKind::XmlError,
context: Box::new(crate::error::ErrorContext {
operation: Some(format!("deserialize {}", std::any::type_name::<T>())),
..Default::default()
}),
source: Some(Box::new(e)),
})
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use super::*;
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct CreateBucketConfiguration {
#[serde(rename = "StorageClass")]
storage_class: String,
#[serde(rename = "DataRedundancyType")]
data_redundancy: String,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename = "Error")]
struct OssErrorXml {
#[serde(rename = "Code")]
code: String,
#[serde(rename = "Message")]
message: String,
#[serde(rename = "RequestId")]
request_id: String,
#[serde(rename = "HostId")]
host_id: String,
#[serde(rename = "BucketName")]
#[serde(default)]
bucket_name: Option<String>,
}
#[derive(Debug, Deserialize, PartialEq)]
struct ListBucketResult {
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Prefix")]
#[serde(default)]
prefix: String,
#[serde(rename = "Marker")]
#[serde(default)]
marker: String,
#[serde(rename = "MaxKeys")]
max_keys: u32,
#[serde(rename = "IsTruncated")]
is_truncated: bool,
#[serde(rename = "Contents", default)]
contents: Vec<ObjectContent>,
}
#[derive(Debug, Deserialize, PartialEq)]
struct ObjectContent {
#[serde(rename = "Key")]
key: String,
#[serde(rename = "Size")]
size: u64,
#[serde(rename = "ETag")]
etag: String,
}
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename = "Delete")]
struct DeleteObjectsRequest {
#[serde(rename = "Quiet", default)]
quiet: bool,
#[serde(rename = "Object")]
objects: Vec<DeleteObject>,
}
#[derive(Debug, Serialize, PartialEq)]
struct DeleteObject {
#[serde(rename = "Key")]
key: String,
}
#[test]
fn serialize_bucket_configuration_to_xml() {
let config = CreateBucketConfiguration {
storage_class: "Standard".into(),
data_redundancy: "LRS".into(),
};
let xml = to_xml(&config).unwrap();
assert!(xml.contains("<StorageClass>Standard</StorageClass>"));
assert!(xml.contains("<DataRedundancyType>LRS</DataRedundancyType>"));
}
#[test]
fn deserialize_error_xml_to_oss_service_error() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchBucket</Code>
<Message>The specified bucket does not exist.</Message>
<RequestId>5D8A8578E44B3E3FD474D789</RequestId>
<HostId>oss-cn-hangzhou.aliyuncs.com</HostId>
<BucketName>non-existent-bucket</BucketName>
</Error>"#;
let error: OssErrorXml = from_xml(xml).unwrap();
assert_eq!(error.code, "NoSuchBucket");
assert_eq!(error.message, "The specified bucket does not exist.");
assert_eq!(error.request_id, "5D8A8578E44B3E3FD474D789");
assert_eq!(error.host_id, "oss-cn-hangzhou.aliyuncs.com");
assert_eq!(error.bucket_name.as_deref(), Some("non-existent-bucket"));
}
#[test]
fn deserialize_list_objects_xml() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult>
<Name>examplebucket</Name>
<Prefix>test</Prefix>
<Marker></Marker>
<MaxKeys>100</MaxKeys>
<IsTruncated>false</IsTruncated>
<Contents>
<Key>test/file1.txt</Key>
<Size>1024</Size>
<ETag>"abc123"</ETag>
</Contents>
<Contents>
<Key>test/file2.txt</Key>
<Size>2048</Size>
<ETag>"def456"</ETag>
</Contents>
</ListBucketResult>"#;
let result: ListBucketResult = from_xml(xml).unwrap();
assert_eq!(result.name, "examplebucket");
assert_eq!(result.prefix, "test");
assert_eq!(result.max_keys, 100);
assert!(!result.is_truncated);
assert_eq!(result.contents.len(), 2);
assert_eq!(result.contents[0].key, "test/file1.txt");
assert_eq!(result.contents[0].size, 1024);
assert_eq!(result.contents[0].etag, "\"abc123\"");
}
#[test]
fn serialize_delete_multiple_objects_xml() {
let request = DeleteObjectsRequest {
quiet: true,
objects: vec![
DeleteObject {
key: "obj1.txt".into(),
},
DeleteObject {
key: "obj2.txt".into(),
},
],
};
let xml = to_xml(&request).unwrap();
assert!(xml.contains("<Delete>"));
assert!(xml.contains("<Quiet>true</Quiet>"));
assert!(xml.contains("<Object><Key>obj1.txt</Key></Object>"));
assert!(xml.contains("<Object><Key>obj2.txt</Key></Object>"));
}
#[test]
fn roundtrip_serialize_deserialize() {
let config = CreateBucketConfiguration {
storage_class: "IA".into(),
data_redundancy: "ZRS".into(),
};
let xml = to_xml(&config).unwrap();
let deserialized: CreateBucketConfiguration = from_xml(&xml).unwrap();
assert_eq!(config, deserialized);
}
#[test]
fn deserialize_missing_optional_fields() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>req-123</RequestId>
<HostId>oss-cn-hangzhou.aliyuncs.com</HostId>
</Error>"#;
let error: OssErrorXml = from_xml(xml).unwrap();
assert_eq!(error.code, "AccessDenied");
assert!(error.bucket_name.is_none());
}
}