use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use crate::ops::common::{StorageClass, Tag};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum LifecycleRuleStatus {
#[default]
Enabled,
Disabled,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct LifecycleExpiration {
pub days: Option<u32>,
pub created_before_date: Option<String>,
pub expired_object_delete_marker: Option<bool>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct LifecycleTransition {
pub days: Option<u32>,
pub created_before_date: Option<String>,
pub storage_class: Option<StorageClass>,
pub is_access_time: Option<bool>,
pub return_to_std_when_visit: Option<bool>,
pub allow_small_file: Option<bool>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct AbortIncompleteMultipartUpload {
pub days: Option<u32>,
pub created_before_date: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct NoncurrentVersionExpiration {
pub noncurrent_days: Option<u32>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct NoncurrentVersionTransition {
pub noncurrent_days: Option<u32>,
pub storage_class: Option<StorageClass>,
pub is_access_time: Option<bool>,
pub return_to_std_when_visit: Option<bool>,
pub allow_small_file: Option<bool>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct LifecycleFilterNot {
pub prefix: Option<String>,
pub tag: Option<Tag>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct LifecycleFilter {
pub not: Option<LifecycleFilterNot>,
pub object_size_greater_than: Option<u64>,
pub object_size_less_than: Option<u64>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct LifecycleRule {
#[serde(rename = "ID", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub prefix: Option<String>,
pub status: LifecycleRuleStatus,
pub expiration: Option<LifecycleExpiration>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub transition: Vec<LifecycleTransition>,
pub abort_multipart_upload: Option<AbortIncompleteMultipartUpload>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tag: Vec<Tag>,
pub noncurrent_version_expiration: Option<NoncurrentVersionExpiration>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub noncurrent_version_transition: Vec<NoncurrentVersionTransition>,
pub filter: Option<LifecycleFilter>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub atime_base: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename = "LifecycleConfiguration")]
pub struct LifecycleConfiguration {
#[serde(rename = "Rule", default, skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<LifecycleRule>,
}
impl LifecycleConfiguration {
pub fn new() -> Self {
Self::default()
}
pub fn with_rules(rules: Vec<LifecycleRule>) -> Self {
Self { rules }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_transition_rule() {
let xml = r#"<LifecycleConfiguration>
<Rule>
<ID>rule</ID>
<Prefix>log/</Prefix>
<Status>Enabled</Status>
<Transition>
<Days>30</Days>
<StorageClass>IA</StorageClass>
</Transition>
</Rule>
</LifecycleConfiguration>"#;
let parsed: LifecycleConfiguration = quick_xml::de::from_str(xml).unwrap();
assert_eq!(parsed.rules.len(), 1);
let rule = &parsed.rules[0];
assert_eq!(rule.id.as_deref(), Some("rule"));
assert_eq!(rule.prefix.as_deref(), Some("log/"));
assert_eq!(rule.status, LifecycleRuleStatus::Enabled);
assert_eq!(rule.transition.len(), 1);
assert_eq!(rule.transition[0].days, Some(30));
assert_eq!(rule.transition[0].storage_class, Some(StorageClass::InfrequentAccess));
}
#[test]
fn parse_multiple_transitions() {
let xml = r#"<LifecycleConfiguration>
<Rule>
<ID>rule</ID>
<Prefix>log/</Prefix>
<Status>Enabled</Status>
<Transition>
<Days>30</Days>
<StorageClass>IA</StorageClass>
</Transition>
<Transition>
<Days>60</Days>
<StorageClass>Archive</StorageClass>
</Transition>
<Expiration>
<Days>3600</Days>
</Expiration>
</Rule>
</LifecycleConfiguration>"#;
let parsed: LifecycleConfiguration = quick_xml::de::from_str(xml).unwrap();
let rule = &parsed.rules[0];
assert_eq!(rule.transition.len(), 2);
assert_eq!(rule.transition[1].storage_class, Some(StorageClass::Archive));
assert_eq!(rule.expiration.as_ref().unwrap().days, Some(3600));
}
#[test]
fn parse_noncurrent_version_expiration_with_delete_marker() {
let xml = r#"<LifecycleConfiguration>
<Rule>
<ID>rule</ID>
<Prefix></Prefix>
<Status>Enabled</Status>
<Expiration>
<ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker>
</Expiration>
<NoncurrentVersionExpiration>
<NoncurrentDays>5</NoncurrentDays>
</NoncurrentVersionExpiration>
</Rule>
</LifecycleConfiguration>"#;
let parsed: LifecycleConfiguration = quick_xml::de::from_str(xml).unwrap();
let rule = &parsed.rules[0];
assert_eq!(rule.expiration.as_ref().unwrap().expired_object_delete_marker, Some(true));
assert_eq!(
rule.noncurrent_version_expiration
.as_ref()
.unwrap()
.noncurrent_days,
Some(5)
);
}
#[test]
fn parse_filter_with_not() {
let xml = r#"<LifecycleConfiguration>
<Rule>
<ID>rule</ID>
<Prefix></Prefix>
<Status>Enabled</Status>
<Filter>
<Not>
<Prefix>log</Prefix>
<Tag><Key>key1</Key><Value>value1</Value></Tag>
</Not>
</Filter>
<Transition>
<Days>30</Days>
<StorageClass>Archive</StorageClass>
</Transition>
<Expiration>
<Days>100</Days>
</Expiration>
</Rule>
</LifecycleConfiguration>"#;
let parsed: LifecycleConfiguration = quick_xml::de::from_str(xml).unwrap();
let rule = &parsed.rules[0];
let not = rule.filter.as_ref().unwrap().not.as_ref().unwrap();
assert_eq!(not.prefix.as_deref(), Some("log"));
assert_eq!(not.tag.as_ref().unwrap().key, "key1");
}
#[test]
fn parse_access_time_with_atime_base() {
let xml = r#"<LifecycleConfiguration>
<Rule>
<ID>atime rule</ID>
<Prefix>logs1/</Prefix>
<Status>Enabled</Status>
<Transition>
<Days>30</Days>
<StorageClass>IA</StorageClass>
<IsAccessTime>true</IsAccessTime>
<ReturnToStdWhenVisit>false</ReturnToStdWhenVisit>
</Transition>
<AtimeBase>1631698332</AtimeBase>
</Rule>
</LifecycleConfiguration>"#;
let parsed: LifecycleConfiguration = quick_xml::de::from_str(xml).unwrap();
let rule = &parsed.rules[0];
assert_eq!(rule.transition[0].is_access_time, Some(true));
assert_eq!(rule.transition[0].return_to_std_when_visit, Some(false));
assert_eq!(rule.atime_base, Some(1631698332));
}
#[test]
fn parse_abort_multipart_upload() {
let xml = r#"<LifecycleConfiguration>
<Rule>
<ID>rule</ID>
<Prefix>/</Prefix>
<Status>Enabled</Status>
<AbortMultipartUpload>
<Days>30</Days>
</AbortMultipartUpload>
</Rule>
</LifecycleConfiguration>"#;
let parsed: LifecycleConfiguration = quick_xml::de::from_str(xml).unwrap();
let amu = parsed.rules[0].abort_multipart_upload.as_ref().unwrap();
assert_eq!(amu.days, Some(30));
}
#[test]
fn serialize_minimal_rule() {
let cfg = LifecycleConfiguration::with_rules(vec![LifecycleRule {
id: Some("rule".to_string()),
prefix: Some("log/".to_string()),
status: LifecycleRuleStatus::Enabled,
expiration: Some(LifecycleExpiration {
days: Some(90),
..Default::default()
}),
..Default::default()
}]);
let xml = quick_xml::se::to_string(&cfg).unwrap();
assert!(xml.contains("<LifecycleConfiguration>"));
assert!(xml.contains("<Rule>"));
assert!(xml.contains("<ID>rule</ID>"));
assert!(xml.contains("<Prefix>log/</Prefix>"));
assert!(xml.contains("<Status>Enabled</Status>"));
assert!(xml.contains("<Days>90</Days>"));
}
#[test]
fn serialize_two_transitions_round_trip() {
let cfg = LifecycleConfiguration::with_rules(vec![LifecycleRule {
id: Some("r".to_string()),
prefix: Some("l/".to_string()),
status: LifecycleRuleStatus::Enabled,
transition: vec![
LifecycleTransition {
days: Some(30),
storage_class: Some(StorageClass::InfrequentAccess),
..Default::default()
},
LifecycleTransition {
days: Some(60),
storage_class: Some(StorageClass::Archive),
..Default::default()
},
],
..Default::default()
}]);
let xml = quick_xml::se::to_string(&cfg).unwrap();
let back: LifecycleConfiguration = quick_xml::de::from_str(&xml).unwrap();
assert_eq!(back, cfg);
}
}