use std::collections::HashMap;
use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LifecycleConfiguration {
pub rules: Vec<LifecycleRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LifecycleRule {
pub id: String,
pub status: LifecycleRuleStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiration: Option<LifecycleExpiration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transition: Option<LifecycleTransition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub noncurrent_version_expiration: Option<NoncurrentVersionExpiration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub noncurrent_version_transition: Option<NoncurrentVersionTransition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub abort_incomplete_multipart_upload_days: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expired_object_delete_marker: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LifecycleRuleStatus {
Enabled,
Disabled,
}
impl fmt::Display for LifecycleRuleStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LifecycleRuleStatus::Enabled => write!(f, "Enabled"),
LifecycleRuleStatus::Disabled => write!(f, "Disabled"),
}
}
}
impl std::str::FromStr for LifecycleRuleStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"enabled" => Ok(LifecycleRuleStatus::Enabled),
"disabled" => Ok(LifecycleRuleStatus::Disabled),
_ => Err(format!("Invalid lifecycle rule status: {s}")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LifecycleExpiration {
#[serde(skip_serializing_if = "Option::is_none")]
pub days: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LifecycleTransition {
#[serde(skip_serializing_if = "Option::is_none")]
pub days: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date: Option<String>,
pub storage_class: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoncurrentVersionExpiration {
pub noncurrent_days: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub newer_noncurrent_versions: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoncurrentVersionTransition {
pub noncurrent_days: i32,
pub storage_class: String,
}
impl fmt::Display for LifecycleRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.id, self.status)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lifecycle_rule_status_display() {
assert_eq!(LifecycleRuleStatus::Enabled.to_string(), "Enabled");
assert_eq!(LifecycleRuleStatus::Disabled.to_string(), "Disabled");
}
#[test]
fn test_lifecycle_rule_status_from_str() {
assert_eq!(
"enabled".parse::<LifecycleRuleStatus>().unwrap(),
LifecycleRuleStatus::Enabled
);
assert_eq!(
"Disabled".parse::<LifecycleRuleStatus>().unwrap(),
LifecycleRuleStatus::Disabled
);
assert!("invalid".parse::<LifecycleRuleStatus>().is_err());
}
#[test]
fn test_lifecycle_rule_serialization() {
let rule = LifecycleRule {
id: "rule-1".to_string(),
status: LifecycleRuleStatus::Enabled,
prefix: Some("logs/".to_string()),
tags: None,
expiration: Some(LifecycleExpiration {
days: Some(30),
date: None,
}),
transition: None,
noncurrent_version_expiration: None,
noncurrent_version_transition: None,
abort_incomplete_multipart_upload_days: Some(7),
expired_object_delete_marker: None,
};
let json = serde_json::to_string(&rule).unwrap();
let decoded: LifecycleRule = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.id, "rule-1");
assert_eq!(decoded.status, LifecycleRuleStatus::Enabled);
assert_eq!(decoded.prefix.as_deref(), Some("logs/"));
assert_eq!(decoded.expiration.as_ref().unwrap().days, Some(30));
assert_eq!(decoded.abort_incomplete_multipart_upload_days, Some(7));
}
#[test]
fn test_lifecycle_transition_serialization() {
let transition = LifecycleTransition {
days: Some(90),
date: None,
storage_class: "WARM_TIER".to_string(),
};
let json = serde_json::to_string(&transition).unwrap();
assert!(json.contains("storageClass"));
assert!(json.contains("WARM_TIER"));
let decoded: LifecycleTransition = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.storage_class, "WARM_TIER");
}
#[test]
fn test_lifecycle_configuration_serialization() {
let config = LifecycleConfiguration {
rules: vec![LifecycleRule {
id: "expire-old".to_string(),
status: LifecycleRuleStatus::Enabled,
prefix: None,
tags: None,
expiration: Some(LifecycleExpiration {
days: Some(365),
date: None,
}),
transition: None,
noncurrent_version_expiration: Some(NoncurrentVersionExpiration {
noncurrent_days: 30,
newer_noncurrent_versions: Some(3),
}),
noncurrent_version_transition: None,
abort_incomplete_multipart_upload_days: None,
expired_object_delete_marker: Some(true),
}],
};
let json = serde_json::to_string_pretty(&config).unwrap();
let decoded: LifecycleConfiguration = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.rules.len(), 1);
assert_eq!(decoded.rules[0].id, "expire-old");
assert_eq!(
decoded.rules[0]
.noncurrent_version_expiration
.as_ref()
.unwrap()
.newer_noncurrent_versions,
Some(3)
);
}
#[test]
fn test_noncurrent_version_transition_serialization() {
let nvt = NoncurrentVersionTransition {
noncurrent_days: 60,
storage_class: "COLD_TIER".to_string(),
};
let json = serde_json::to_string(&nvt).unwrap();
let decoded: NoncurrentVersionTransition = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.noncurrent_days, 60);
assert_eq!(decoded.storage_class, "COLD_TIER");
}
}