Skip to main content

rc_core/
lifecycle.rs

1//! Lifecycle (ILM) configuration types
2//!
3//! Domain types for S3 bucket lifecycle rules including expiration,
4//! transition, and noncurrent version management.
5
6use std::collections::HashMap;
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11/// Full lifecycle configuration for a bucket
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct LifecycleConfiguration {
14    /// Lifecycle rules
15    pub rules: Vec<LifecycleRule>,
16}
17
18/// A single lifecycle rule
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct LifecycleRule {
22    /// Rule identifier
23    pub id: String,
24
25    /// Whether the rule is enabled or disabled
26    pub status: LifecycleRuleStatus,
27
28    /// Key prefix filter
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub prefix: Option<String>,
31
32    /// Tag-based filter (key=value pairs)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub tags: Option<HashMap<String, String>>,
35
36    /// Expiration settings for current object versions
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub expiration: Option<LifecycleExpiration>,
39
40    /// Transition settings for current object versions
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub transition: Option<LifecycleTransition>,
43
44    /// Expiration settings for noncurrent object versions
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub noncurrent_version_expiration: Option<NoncurrentVersionExpiration>,
47
48    /// Transition settings for noncurrent object versions
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub noncurrent_version_transition: Option<NoncurrentVersionTransition>,
51
52    /// Days after initiation to abort incomplete multipart uploads
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub abort_incomplete_multipart_upload_days: Option<i32>,
55
56    /// Whether to remove expired delete markers
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub expired_object_delete_marker: Option<bool>,
59}
60
61/// Rule status
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63pub enum LifecycleRuleStatus {
64    Enabled,
65    Disabled,
66}
67
68impl fmt::Display for LifecycleRuleStatus {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            LifecycleRuleStatus::Enabled => write!(f, "Enabled"),
72            LifecycleRuleStatus::Disabled => write!(f, "Disabled"),
73        }
74    }
75}
76
77impl std::str::FromStr for LifecycleRuleStatus {
78    type Err = String;
79
80    fn from_str(s: &str) -> Result<Self, Self::Err> {
81        match s.to_lowercase().as_str() {
82            "enabled" => Ok(LifecycleRuleStatus::Enabled),
83            "disabled" => Ok(LifecycleRuleStatus::Disabled),
84            _ => Err(format!("Invalid lifecycle rule status: {s}")),
85        }
86    }
87}
88
89/// Expiration settings for current object versions
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct LifecycleExpiration {
92    /// Number of days after creation to expire
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub days: Option<i32>,
95
96    /// Specific date to expire (ISO 8601 format)
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub date: Option<String>,
99}
100
101/// Transition settings for current object versions
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct LifecycleTransition {
105    /// Number of days after creation to transition
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub days: Option<i32>,
108
109    /// Specific date to transition (ISO 8601 format)
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub date: Option<String>,
112
113    /// Target storage class (tier name)
114    pub storage_class: String,
115}
116
117/// Expiration settings for noncurrent object versions
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct NoncurrentVersionExpiration {
121    /// Number of days after becoming noncurrent to expire
122    pub noncurrent_days: i32,
123
124    /// Maximum number of noncurrent versions to retain
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub newer_noncurrent_versions: Option<i32>,
127}
128
129/// Transition settings for noncurrent object versions
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct NoncurrentVersionTransition {
133    /// Number of days after becoming noncurrent to transition
134    pub noncurrent_days: i32,
135
136    /// Target storage class (tier name)
137    pub storage_class: String,
138}
139
140impl fmt::Display for LifecycleRule {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(f, "{} ({})", self.id, self.status)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_lifecycle_rule_status_display() {
152        assert_eq!(LifecycleRuleStatus::Enabled.to_string(), "Enabled");
153        assert_eq!(LifecycleRuleStatus::Disabled.to_string(), "Disabled");
154    }
155
156    #[test]
157    fn test_lifecycle_rule_status_from_str() {
158        assert_eq!(
159            "enabled".parse::<LifecycleRuleStatus>().unwrap(),
160            LifecycleRuleStatus::Enabled
161        );
162        assert_eq!(
163            "Disabled".parse::<LifecycleRuleStatus>().unwrap(),
164            LifecycleRuleStatus::Disabled
165        );
166        assert!("invalid".parse::<LifecycleRuleStatus>().is_err());
167    }
168
169    #[test]
170    fn test_lifecycle_rule_serialization() {
171        let rule = LifecycleRule {
172            id: "rule-1".to_string(),
173            status: LifecycleRuleStatus::Enabled,
174            prefix: Some("logs/".to_string()),
175            tags: None,
176            expiration: Some(LifecycleExpiration {
177                days: Some(30),
178                date: None,
179            }),
180            transition: None,
181            noncurrent_version_expiration: None,
182            noncurrent_version_transition: None,
183            abort_incomplete_multipart_upload_days: Some(7),
184            expired_object_delete_marker: None,
185        };
186
187        let json = serde_json::to_string(&rule).unwrap();
188        let decoded: LifecycleRule = serde_json::from_str(&json).unwrap();
189        assert_eq!(decoded.id, "rule-1");
190        assert_eq!(decoded.status, LifecycleRuleStatus::Enabled);
191        assert_eq!(decoded.prefix.as_deref(), Some("logs/"));
192        assert_eq!(decoded.expiration.as_ref().unwrap().days, Some(30));
193        assert_eq!(decoded.abort_incomplete_multipart_upload_days, Some(7));
194    }
195
196    #[test]
197    fn test_lifecycle_transition_serialization() {
198        let transition = LifecycleTransition {
199            days: Some(90),
200            date: None,
201            storage_class: "WARM_TIER".to_string(),
202        };
203
204        let json = serde_json::to_string(&transition).unwrap();
205        assert!(json.contains("storageClass"));
206        assert!(json.contains("WARM_TIER"));
207
208        let decoded: LifecycleTransition = serde_json::from_str(&json).unwrap();
209        assert_eq!(decoded.storage_class, "WARM_TIER");
210    }
211
212    #[test]
213    fn test_lifecycle_configuration_serialization() {
214        let config = LifecycleConfiguration {
215            rules: vec![LifecycleRule {
216                id: "expire-old".to_string(),
217                status: LifecycleRuleStatus::Enabled,
218                prefix: None,
219                tags: None,
220                expiration: Some(LifecycleExpiration {
221                    days: Some(365),
222                    date: None,
223                }),
224                transition: None,
225                noncurrent_version_expiration: Some(NoncurrentVersionExpiration {
226                    noncurrent_days: 30,
227                    newer_noncurrent_versions: Some(3),
228                }),
229                noncurrent_version_transition: None,
230                abort_incomplete_multipart_upload_days: None,
231                expired_object_delete_marker: Some(true),
232            }],
233        };
234
235        let json = serde_json::to_string_pretty(&config).unwrap();
236        let decoded: LifecycleConfiguration = serde_json::from_str(&json).unwrap();
237        assert_eq!(decoded.rules.len(), 1);
238        assert_eq!(decoded.rules[0].id, "expire-old");
239        assert_eq!(
240            decoded.rules[0]
241                .noncurrent_version_expiration
242                .as_ref()
243                .unwrap()
244                .newer_noncurrent_versions,
245            Some(3)
246        );
247    }
248
249    #[test]
250    fn test_noncurrent_version_transition_serialization() {
251        let nvt = NoncurrentVersionTransition {
252            noncurrent_days: 60,
253            storage_class: "COLD_TIER".to_string(),
254        };
255
256        let json = serde_json::to_string(&nvt).unwrap();
257        let decoded: NoncurrentVersionTransition = serde_json::from_str(&json).unwrap();
258        assert_eq!(decoded.noncurrent_days, 60);
259        assert_eq!(decoded.storage_class, "COLD_TIER");
260    }
261}