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