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