1use std::collections::HashMap;
28use std::time::Duration;
29
30use objectstore_types::duration::format_duration;
31use objectstore_types::metadata::{ExpirationPolicy, Metadata};
32use serde::{Deserialize, Serialize};
33use thiserror::Error;
34
35#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)]
40pub struct UseCases(pub HashMap<String, UseCaseConfig>);
41
42impl UseCases {
43 pub fn validate(&self, usecase: &str, metadata: &Metadata) -> Result<(), UseCaseError> {
48 if let Some(config) = self.0.get(usecase) {
49 config.validate(usecase, metadata)?
50 }
51 Ok(())
52 }
53
54 pub fn get_max_expiry(&self, usecase: &str) -> Option<Duration> {
58 self.0.get(usecase).and_then(|config| config.expiration.max)
59 }
60}
61
62#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)]
64#[serde(default)]
65pub struct UseCaseConfig {
66 pub expiration: ExpirationConfig,
68}
69
70impl UseCaseConfig {
71 fn validate(&self, usecase: &str, metadata: &Metadata) -> Result<(), UseCaseError> {
72 let policy = metadata.expiration_policy;
73 let allowed = match policy {
74 ExpirationPolicy::Manual => self.expiration.manual.allowed,
75 ExpirationPolicy::TimeToLive(_) => self.expiration.ttl.allowed,
76 ExpirationPolicy::TimeToIdle(_) => self.expiration.tti.allowed,
77 };
78 if !allowed {
79 return Err(UseCaseError::PolicyNotAllowed {
80 usecase: usecase.to_owned(),
81 policy,
82 });
83 }
84
85 if let Some(max) = self.expiration.max
86 && let Some(duration) = policy.expires_in()
87 && duration > max
88 {
89 return Err(UseCaseError::DurationExceeded {
90 usecase: usecase.to_owned(),
91 duration: format_duration(duration).to_string(),
92 max: format_duration(max).to_string(),
93 });
94 }
95 Ok(())
96 }
97}
98
99#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)]
101#[serde(default)]
102pub struct ExpirationConfig {
103 pub manual: PolicyConfig,
105 pub ttl: PolicyConfig,
107 pub tti: PolicyConfig,
109 #[serde(default, with = "humantime_serde")]
114 pub max: Option<Duration>,
115}
116
117#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
119#[serde(default)]
120pub struct PolicyConfig {
121 pub allowed: bool,
123}
124
125impl Default for PolicyConfig {
126 fn default() -> Self {
127 Self { allowed: true }
128 }
129}
130
131#[derive(Debug, Error)]
133pub enum UseCaseError {
134 #[error("expiration policy '{policy}' is not allowed for use case '{usecase}'")]
136 PolicyNotAllowed {
137 usecase: String,
139 policy: ExpirationPolicy,
141 },
142
143 #[error("expiration duration {duration} exceeds maximum of {max} for use case '{usecase}'")]
145 DurationExceeded {
146 usecase: String,
148 duration: String,
150 max: String,
152 },
153}
154
155#[cfg(test)]
156mod tests {
157 use std::time::Duration;
158
159 use objectstore_types::metadata::{ExpirationPolicy, Metadata};
160
161 use super::*;
162
163 fn make_metadata(policy: ExpirationPolicy) -> Metadata {
164 Metadata {
165 expiration_policy: policy,
166 ..Metadata::default()
167 }
168 }
169
170 fn usecases_from(config: UseCaseConfig) -> UseCases {
171 let mut map = HashMap::new();
172 map.insert("test".to_owned(), config);
173 UseCases(map)
174 }
175
176 #[test]
179 fn unconfigured_usecase_allows_manual() {
180 let usecases = UseCases::default();
181 let metadata = make_metadata(ExpirationPolicy::Manual);
182 usecases.validate("anything", &metadata).unwrap();
183 }
184
185 #[test]
186 fn unconfigured_usecase_allows_ttl() {
187 let usecases = UseCases::default();
188 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
189 usecases.validate("anything", &metadata).unwrap();
190 }
191
192 #[test]
193 fn unconfigured_usecase_allows_tti() {
194 let usecases = UseCases::default();
195 let metadata = make_metadata(ExpirationPolicy::TimeToIdle(Duration::from_hours(1)));
196 usecases.validate("anything", &metadata).unwrap();
197 }
198
199 #[test]
202 fn manual_disallowed_rejects() {
203 let usecases = usecases_from(UseCaseConfig {
204 expiration: ExpirationConfig {
205 manual: PolicyConfig { allowed: false },
206 ..ExpirationConfig::default()
207 },
208 });
209
210 let metadata = make_metadata(ExpirationPolicy::Manual);
211 let err = usecases.validate("test", &metadata).unwrap_err();
212 assert!(matches!(err, UseCaseError::PolicyNotAllowed { .. }));
213 }
214
215 #[test]
216 fn manual_allowed_passes() {
217 let usecases = usecases_from(UseCaseConfig {
218 expiration: ExpirationConfig {
219 manual: PolicyConfig { allowed: true },
220 max: Some(Duration::ZERO),
221 ..ExpirationConfig::default()
222 },
223 });
224
225 let metadata = make_metadata(ExpirationPolicy::Manual);
226 usecases.validate("test", &metadata).unwrap();
227 }
228
229 #[test]
232 fn ttl_disallowed_rejects() {
233 let usecases = usecases_from(UseCaseConfig {
234 expiration: ExpirationConfig {
235 ttl: PolicyConfig { allowed: false },
236 ..ExpirationConfig::default()
237 },
238 });
239
240 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
241 let err = usecases.validate("test", &metadata).unwrap_err();
242 assert!(matches!(err, UseCaseError::PolicyNotAllowed { .. }));
243 }
244
245 #[test]
246 fn ttl_within_max_passes() {
247 let usecases = usecases_from(UseCaseConfig {
248 expiration: ExpirationConfig {
249 max: Some(Duration::from_hours(2)),
250 ..ExpirationConfig::default()
251 },
252 });
253
254 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
255 usecases.validate("test", &metadata).unwrap();
256 }
257
258 #[test]
259 fn ttl_at_max_passes() {
260 let usecases = usecases_from(UseCaseConfig {
261 expiration: ExpirationConfig {
262 max: Some(Duration::from_hours(1)),
263 ..ExpirationConfig::default()
264 },
265 });
266
267 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
268 usecases.validate("test", &metadata).unwrap();
269 }
270
271 #[test]
272 fn ttl_exceeds_max_rejects() {
273 let usecases = usecases_from(UseCaseConfig {
274 expiration: ExpirationConfig {
275 max: Some(Duration::from_hours(1)),
276 ..ExpirationConfig::default()
277 },
278 });
279
280 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(2)));
281 let err = usecases.validate("test", &metadata).unwrap_err();
282 assert!(matches!(err, UseCaseError::DurationExceeded { .. }));
283 }
284
285 #[test]
288 fn tti_disallowed_rejects() {
289 let usecases = usecases_from(UseCaseConfig {
290 expiration: ExpirationConfig {
291 tti: PolicyConfig { allowed: false },
292 ..ExpirationConfig::default()
293 },
294 });
295
296 let metadata = make_metadata(ExpirationPolicy::TimeToIdle(Duration::from_hours(1)));
297 let err = usecases.validate("test", &metadata).unwrap_err();
298 assert!(matches!(err, UseCaseError::PolicyNotAllowed { .. }));
299 }
300
301 #[test]
302 fn tti_within_max_passes() {
303 let usecases = usecases_from(UseCaseConfig {
304 expiration: ExpirationConfig {
305 max: Some(Duration::from_hours(2)),
306 ..ExpirationConfig::default()
307 },
308 });
309
310 let metadata = make_metadata(ExpirationPolicy::TimeToIdle(Duration::from_hours(1)));
311 usecases.validate("test", &metadata).unwrap();
312 }
313
314 #[test]
315 fn tti_exceeds_max_rejects() {
316 let usecases = usecases_from(UseCaseConfig {
317 expiration: ExpirationConfig {
318 max: Some(Duration::from_hours(1)),
319 ..ExpirationConfig::default()
320 },
321 });
322
323 let metadata = make_metadata(ExpirationPolicy::TimeToIdle(Duration::from_hours(2)));
324 let err = usecases.validate("test", &metadata).unwrap_err();
325 assert!(matches!(err, UseCaseError::DurationExceeded { .. }));
326 }
327
328 #[test]
331 fn other_policies_unaffected_when_only_tti_restricted() {
332 let usecases = usecases_from(UseCaseConfig {
333 expiration: ExpirationConfig {
334 tti: PolicyConfig { allowed: false },
335 ..ExpirationConfig::default()
336 },
337 });
338
339 let metadata = make_metadata(ExpirationPolicy::Manual);
340 usecases.validate("test", &metadata).unwrap();
341
342 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
343 usecases.validate("test", &metadata).unwrap();
344 }
345}