Skip to main content

reduct_base/msg/
lifecycle_api.rs

1// Copyright 2021-2026 ReductSoftware UG
2// Licensed under the Apache License, Version 2.0
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// Lifecycle policy action type.
9#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
10#[serde(rename_all = "lowercase")]
11pub enum LifecycleType {
12    /// Delete records matched by lifecycle settings.
13    #[default]
14    Delete,
15    /// Compress blocks matched by lifecycle settings.
16    Compress,
17}
18
19/// Lifecycle mode.
20#[repr(u8)]
21#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
22#[serde(rename_all = "lowercase")]
23pub enum LifecycleMode {
24    /// Lifecycle is active and executes actions.
25    Enabled,
26    /// Lifecycle is inactive and does not execute actions.
27    Disabled,
28    /// Lifecycle runs in preview mode and does not remove records.
29    #[serde(rename = "dry_run")]
30    DryRun,
31}
32
33impl Default for LifecycleMode {
34    fn default() -> Self {
35        Self::Enabled
36    }
37}
38
39/// Lifecycle policy settings.
40#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
41pub struct LifecycleSettings {
42    /// Lifecycle policy action type.
43    #[serde(rename = "type")]
44    pub lifecycle_type: LifecycleType,
45    /// Bucket to apply the lifecycle policy to.
46    pub bucket: String,
47    /// Entries to clean. If empty, all removable entries are matched. Supports exact names,
48    /// glob-like `*` and `**` wildcards, and `!` exclusion patterns, consistent with query and
49    /// replication entry filters. System metadata entries (e.g. `entry/$meta`) are always excluded.
50    #[serde(default)]
51    pub entries: Vec<String>,
52    /// Records older than this duration, e.g. "30d", "24h", or "3600s".
53    pub older_than: String,
54    /// Interval between lifecycle runs, e.g. "30m", "1h", or "3600s".
55    #[serde(default = "default_lifecycle_interval")]
56    pub interval: String,
57    /// When condition.
58    #[serde(default)]
59    pub when: Option<Value>,
60    /// Maximum timestamp range processed in one run, e.g. "12h" or "1d".
61    /// If not set, the run window defaults to 24 times the interval.
62    #[serde(default)]
63    pub processing_interval: Option<String>,
64    /// Lifecycle mode.
65    #[serde(default)]
66    pub mode: LifecycleMode,
67}
68
69impl Default for LifecycleSettings {
70    fn default() -> Self {
71        Self {
72            lifecycle_type: LifecycleType::default(),
73            bucket: String::default(),
74            entries: Vec::default(),
75            older_than: String::default(),
76            interval: default_lifecycle_interval(),
77            when: Option::default(),
78            processing_interval: Option::default(),
79            mode: LifecycleMode::default(),
80        }
81    }
82}
83
84fn default_lifecycle_interval() -> String {
85    "3600s".to_string()
86}
87
88/// Lifecycle policy information.
89#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
90pub struct LifecycleInfo {
91    /// Lifecycle policy name.
92    pub name: String,
93    /// Lifecycle policy is provisioned.
94    pub is_provisioned: bool,
95    /// Lifecycle worker is running.
96    pub is_running: bool,
97    /// Lifecycle policy action type.
98    #[serde(default, rename = "type")]
99    pub lifecycle_type: LifecycleType,
100    /// Lifecycle mode.
101    #[serde(default)]
102    pub mode: LifecycleMode,
103    /// Last lifecycle run timestamp.
104    #[serde(default)]
105    pub last_run: Option<DateTime<Utc>>,
106}
107
108/// Payload for updating lifecycle mode.
109#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
110pub struct LifecycleModePayload {
111    pub mode: LifecycleMode,
112}
113
114/// Lifecycle policy list.
115#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
116pub struct LifecycleList {
117    /// Lifecycle policies.
118    pub lifecycles: Vec<LifecycleInfo>,
119}
120
121/// Full lifecycle policy information.
122#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
123pub struct FullLifecycleInfo {
124    /// Info.
125    pub info: LifecycleInfo,
126    /// Settings.
127    pub settings: LifecycleSettings,
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn lifecycle_mode_default_is_enabled() {
136        assert_eq!(LifecycleMode::default(), LifecycleMode::Enabled);
137    }
138
139    #[test]
140    fn lifecycle_mode_serde_roundtrip() {
141        assert_eq!(
142            serde_json::to_string(&LifecycleMode::Enabled).unwrap(),
143            "\"enabled\""
144        );
145        assert_eq!(
146            serde_json::to_string(&LifecycleMode::Disabled).unwrap(),
147            "\"disabled\""
148        );
149        assert_eq!(
150            serde_json::to_string(&LifecycleMode::DryRun).unwrap(),
151            "\"dry_run\""
152        );
153
154        assert_eq!(
155            serde_json::from_str::<LifecycleMode>("\"enabled\"").unwrap(),
156            LifecycleMode::Enabled
157        );
158        assert_eq!(
159            serde_json::from_str::<LifecycleMode>("\"disabled\"").unwrap(),
160            LifecycleMode::Disabled
161        );
162        assert_eq!(
163            serde_json::from_str::<LifecycleMode>("\"dry_run\"").unwrap(),
164            LifecycleMode::DryRun
165        );
166    }
167
168    #[test]
169    fn lifecycle_settings_mode_defaults_on_missing() {
170        let settings: LifecycleSettings = serde_json::from_str(
171            r#"{
172                "type": "delete",
173                "bucket": "bucket-1",
174                "entries": ["entry-1"],
175                "older_than": "1d",
176                "interval": "1h"
177            }"#,
178        )
179        .unwrap();
180
181        assert_eq!(settings.mode, LifecycleMode::Enabled);
182    }
183
184    #[test]
185    fn lifecycle_settings_processing_interval_defaults_to_none() {
186        let settings: LifecycleSettings = serde_json::from_str(
187            r#"{
188                "type": "delete",
189                "bucket": "bucket-1",
190                "older_than": "1d"
191            }"#,
192        )
193        .unwrap();
194
195        assert_eq!(settings.processing_interval, None);
196    }
197
198    #[test]
199    fn lifecycle_settings_processing_interval_roundtrip() {
200        let settings: LifecycleSettings = serde_json::from_str(
201            r#"{
202                "type": "delete",
203                "bucket": "bucket-1",
204                "older_than": "1d",
205                "processing_interval": "12h"
206            }"#,
207        )
208        .unwrap();
209
210        assert_eq!(settings.processing_interval, Some("12h".to_string()));
211
212        let serialized = serde_json::to_string(&settings).unwrap();
213        let deserialized: LifecycleSettings = serde_json::from_str(&serialized).unwrap();
214        assert_eq!(deserialized, settings);
215    }
216
217    #[test]
218    fn lifecycle_settings_type_is_required() {
219        let err = serde_json::from_str::<LifecycleSettings>(
220            r#"{
221                "bucket": "bucket-1",
222                "older_than": "1d"
223            }"#,
224        )
225        .unwrap_err();
226
227        assert!(err.to_string().contains("missing field `type`"));
228    }
229
230    #[test]
231    fn lifecycle_settings_default_interval_is_3600s() {
232        let settings = LifecycleSettings::default();
233        assert_eq!(settings.interval, "3600s");
234    }
235
236    #[test]
237    fn lifecycle_info_mode_defaults_on_missing() {
238        let info: LifecycleInfo = serde_json::from_str(
239            r#"{
240                "name": "test",
241                "is_provisioned": false,
242                "is_running": true
243            }"#,
244        )
245        .unwrap();
246
247        assert_eq!(info.lifecycle_type, LifecycleType::Delete);
248        assert_eq!(info.mode, LifecycleMode::Enabled);
249        assert_eq!(info.last_run, None);
250    }
251
252    #[test]
253    fn lifecycle_mode_payload_serde_roundtrip() {
254        let payload = LifecycleModePayload {
255            mode: LifecycleMode::Disabled,
256        };
257
258        let serialized = serde_json::to_string(&payload).unwrap();
259        assert_eq!(serialized, r#"{"mode":"disabled"}"#);
260
261        let deserialized: LifecycleModePayload = serde_json::from_str(&serialized).unwrap();
262        assert_eq!(deserialized, payload);
263    }
264}