1use std::collections::HashMap;
30use std::time::Duration;
31
32use objectstore_types::duration::format_duration;
33use objectstore_types::metadata::{ExpirationPolicy, Metadata};
34use serde::{Deserialize, Serialize};
35use thiserror::Error;
36
37#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)]
42pub struct UseCases(pub HashMap<String, UseCaseConfig>);
43
44impl UseCases {
45 pub fn validate(&self, usecase: &str, metadata: &Metadata) -> Result<(), UseCaseError> {
50 if let Some(config) = self.0.get(usecase) {
51 config.validate(usecase, metadata)?
52 }
53 Ok(())
54 }
55}
56
57#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)]
59#[serde(default)]
60pub struct UseCaseConfig {
61 pub expiration: ExpirationConfig,
63}
64
65impl UseCaseConfig {
66 fn validate(&self, usecase: &str, metadata: &Metadata) -> Result<(), UseCaseError> {
67 match metadata.expiration_policy {
68 ExpirationPolicy::Manual => {
69 if !self.expiration.manual.allowed {
70 return Err(UseCaseError::PolicyNotAllowed {
71 usecase: usecase.to_owned(),
72 policy: metadata.expiration_policy,
73 });
74 }
75 }
76 ExpirationPolicy::TimeToLive(duration) => {
77 if !self.expiration.ttl.allowed {
78 return Err(UseCaseError::PolicyNotAllowed {
79 usecase: usecase.to_owned(),
80 policy: metadata.expiration_policy,
81 });
82 }
83 if let Some(max) = self.expiration.ttl.max
84 && duration > max
85 {
86 return Err(UseCaseError::DurationExceeded {
87 usecase: usecase.to_owned(),
88 duration: format_duration(duration).to_string(),
89 max: format_duration(max).to_string(),
90 });
91 }
92 }
93 ExpirationPolicy::TimeToIdle(duration) => {
94 if !self.expiration.tti.allowed {
95 return Err(UseCaseError::PolicyNotAllowed {
96 usecase: usecase.to_owned(),
97 policy: metadata.expiration_policy,
98 });
99 }
100 if let Some(max) = self.expiration.tti.max
101 && duration > max
102 {
103 return Err(UseCaseError::DurationExceeded {
104 usecase: usecase.to_owned(),
105 duration: format_duration(duration).to_string(),
106 max: format_duration(max).to_string(),
107 });
108 }
109 }
110 }
111 Ok(())
112 }
113}
114
115#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)]
117#[serde(default)]
118pub struct ExpirationConfig {
119 pub manual: ManualPolicyConfig,
121 pub ttl: DurationPolicyConfig,
123 pub tti: DurationPolicyConfig,
125}
126
127#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
129#[serde(default)]
130pub struct ManualPolicyConfig {
131 pub allowed: bool,
133}
134
135impl Default for ManualPolicyConfig {
136 fn default() -> Self {
137 Self { allowed: true }
138 }
139}
140
141#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
143#[serde(default)]
144pub struct DurationPolicyConfig {
145 pub allowed: bool,
147 #[serde(default, with = "humantime_serde")]
149 pub max: Option<Duration>,
150}
151
152impl Default for DurationPolicyConfig {
153 fn default() -> Self {
154 Self {
155 allowed: true,
156 max: None,
157 }
158 }
159}
160
161#[derive(Debug, Error)]
163pub enum UseCaseError {
164 #[error("expiration policy '{policy}' is not allowed for use case '{usecase}'")]
166 PolicyNotAllowed {
167 usecase: String,
169 policy: ExpirationPolicy,
171 },
172
173 #[error("expiration duration {duration} exceeds maximum of {max} for use case '{usecase}'")]
175 DurationExceeded {
176 usecase: String,
178 duration: String,
180 max: String,
182 },
183}
184
185#[cfg(test)]
186mod tests {
187 use std::time::Duration;
188
189 use objectstore_types::metadata::{ExpirationPolicy, Metadata};
190
191 use super::*;
192
193 fn make_metadata(policy: ExpirationPolicy) -> Metadata {
194 Metadata {
195 expiration_policy: policy,
196 ..Metadata::default()
197 }
198 }
199
200 fn usecases_from(config: UseCaseConfig) -> UseCases {
201 let mut map = HashMap::new();
202 map.insert("test".to_owned(), config);
203 UseCases(map)
204 }
205
206 #[test]
209 fn unconfigured_usecase_allows_manual() {
210 let usecases = UseCases::default();
211 let metadata = make_metadata(ExpirationPolicy::Manual);
212 usecases.validate("anything", &metadata).unwrap();
213 }
214
215 #[test]
216 fn unconfigured_usecase_allows_ttl() {
217 let usecases = UseCases::default();
218 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
219 usecases.validate("anything", &metadata).unwrap();
220 }
221
222 #[test]
223 fn unconfigured_usecase_allows_tti() {
224 let usecases = UseCases::default();
225 let metadata = make_metadata(ExpirationPolicy::TimeToIdle(Duration::from_hours(1)));
226 usecases.validate("anything", &metadata).unwrap();
227 }
228
229 #[test]
232 fn manual_disallowed_rejects() {
233 let usecases = usecases_from(UseCaseConfig {
234 expiration: ExpirationConfig {
235 manual: ManualPolicyConfig { allowed: false },
236 ..ExpirationConfig::default()
237 },
238 });
239
240 let metadata = make_metadata(ExpirationPolicy::Manual);
241 let err = usecases.validate("test", &metadata).unwrap_err();
242 assert!(matches!(err, UseCaseError::PolicyNotAllowed { .. }));
243 }
244
245 #[test]
246 fn manual_allowed_passes() {
247 let usecases = usecases_from(UseCaseConfig {
248 expiration: ExpirationConfig {
249 manual: ManualPolicyConfig { allowed: true },
250 ..ExpirationConfig::default()
251 },
252 });
253
254 let metadata = make_metadata(ExpirationPolicy::Manual);
255 usecases.validate("test", &metadata).unwrap();
256 }
257
258 #[test]
261 fn ttl_disallowed_rejects() {
262 let usecases = usecases_from(UseCaseConfig {
263 expiration: ExpirationConfig {
264 ttl: DurationPolicyConfig {
265 allowed: false,
266 max: None,
267 },
268 ..ExpirationConfig::default()
269 },
270 });
271
272 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
273 let err = usecases.validate("test", &metadata).unwrap_err();
274 assert!(matches!(err, UseCaseError::PolicyNotAllowed { .. }));
275 }
276
277 #[test]
278 fn ttl_within_max_passes() {
279 let usecases = usecases_from(UseCaseConfig {
280 expiration: ExpirationConfig {
281 ttl: DurationPolicyConfig {
282 allowed: true,
283 max: Some(Duration::from_hours(2)),
284 },
285 ..ExpirationConfig::default()
286 },
287 });
288
289 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
290 usecases.validate("test", &metadata).unwrap();
291 }
292
293 #[test]
294 fn ttl_at_max_passes() {
295 let usecases = usecases_from(UseCaseConfig {
296 expiration: ExpirationConfig {
297 ttl: DurationPolicyConfig {
298 allowed: true,
299 max: Some(Duration::from_hours(1)),
300 },
301 ..ExpirationConfig::default()
302 },
303 });
304
305 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
306 usecases.validate("test", &metadata).unwrap();
307 }
308
309 #[test]
310 fn ttl_exceeds_max_rejects() {
311 let usecases = usecases_from(UseCaseConfig {
312 expiration: ExpirationConfig {
313 ttl: DurationPolicyConfig {
314 allowed: true,
315 max: Some(Duration::from_hours(1)),
316 },
317 ..ExpirationConfig::default()
318 },
319 });
320
321 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(2)));
322 let err = usecases.validate("test", &metadata).unwrap_err();
323 assert!(matches!(err, UseCaseError::DurationExceeded { .. }));
324 }
325
326 #[test]
329 fn tti_disallowed_rejects() {
330 let usecases = usecases_from(UseCaseConfig {
331 expiration: ExpirationConfig {
332 tti: DurationPolicyConfig {
333 allowed: false,
334 max: None,
335 },
336 ..ExpirationConfig::default()
337 },
338 });
339
340 let metadata = make_metadata(ExpirationPolicy::TimeToIdle(Duration::from_hours(1)));
341 let err = usecases.validate("test", &metadata).unwrap_err();
342 assert!(matches!(err, UseCaseError::PolicyNotAllowed { .. }));
343 }
344
345 #[test]
346 fn tti_within_max_passes() {
347 let usecases = usecases_from(UseCaseConfig {
348 expiration: ExpirationConfig {
349 tti: DurationPolicyConfig {
350 allowed: true,
351 max: Some(Duration::from_hours(2)),
352 },
353 ..ExpirationConfig::default()
354 },
355 });
356
357 let metadata = make_metadata(ExpirationPolicy::TimeToIdle(Duration::from_hours(1)));
358 usecases.validate("test", &metadata).unwrap();
359 }
360
361 #[test]
362 fn tti_exceeds_max_rejects() {
363 let usecases = usecases_from(UseCaseConfig {
364 expiration: ExpirationConfig {
365 tti: DurationPolicyConfig {
366 allowed: true,
367 max: Some(Duration::from_hours(1)),
368 },
369 ..ExpirationConfig::default()
370 },
371 });
372
373 let metadata = make_metadata(ExpirationPolicy::TimeToIdle(Duration::from_hours(2)));
374 let err = usecases.validate("test", &metadata).unwrap_err();
375 assert!(matches!(err, UseCaseError::DurationExceeded { .. }));
376 }
377
378 #[test]
381 fn other_policies_unaffected_when_only_tti_restricted() {
382 let usecases = usecases_from(UseCaseConfig {
383 expiration: ExpirationConfig {
384 tti: DurationPolicyConfig {
385 allowed: false,
386 max: None,
387 },
388 ..ExpirationConfig::default()
389 },
390 });
391
392 let metadata = make_metadata(ExpirationPolicy::Manual);
393 usecases.validate("test", &metadata).unwrap();
394
395 let metadata = make_metadata(ExpirationPolicy::TimeToLive(Duration::from_hours(1)));
396 usecases.validate("test", &metadata).unwrap();
397 }
398}