sync_engine/
submit_options.rs1use std::time::Duration;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum CacheTtl {
48 Minute,
50 Short,
52 Medium,
54 Hour,
56 Day,
58 Week,
60 Custom(u64),
62}
63
64impl CacheTtl {
65 #[must_use]
70 pub fn custom_secs(secs: u64) -> Self {
71 Self::Custom(secs)
72 }
73
74 #[must_use]
76 pub fn to_duration(self) -> Duration {
77 match self {
78 CacheTtl::Minute => Duration::from_secs(60),
79 CacheTtl::Short => Duration::from_secs(5 * 60),
80 CacheTtl::Medium => Duration::from_secs(15 * 60),
81 CacheTtl::Hour => Duration::from_secs(60 * 60),
82 CacheTtl::Day => Duration::from_secs(24 * 60 * 60),
83 CacheTtl::Week => Duration::from_secs(7 * 24 * 60 * 60),
84 CacheTtl::Custom(secs) => Duration::from_secs(secs),
85 }
86 }
87
88 #[must_use]
90 pub fn as_secs(self) -> u64 {
91 self.to_duration().as_secs()
92 }
93}
94
95impl From<Duration> for CacheTtl {
96 fn from(d: Duration) -> Self {
98 let secs = d.as_secs();
99 match secs {
100 0..=90 => CacheTtl::Minute, 91..=450 => CacheTtl::Short, 451..=2700 => CacheTtl::Medium, 2701..=5400 => CacheTtl::Hour, 5401..=129600 => CacheTtl::Day, 129601..=864000 => CacheTtl::Week, _ => CacheTtl::Custom(secs), }
108 }
109}
110
111#[derive(Debug, Clone)]
121pub struct SubmitOptions {
122 pub redis: bool,
126
127 pub redis_ttl: Option<CacheTtl>,
133
134 pub sql: bool,
138
139 pub state: Option<String>,
146}
147
148impl Default for SubmitOptions {
149 fn default() -> Self {
150 Self {
151 redis: true,
152 redis_ttl: None,
153 sql: true,
154 state: None,
155 }
156 }
157}
158
159impl SubmitOptions {
160 #[must_use]
180 pub fn cache(ttl: CacheTtl) -> Self {
181 Self {
182 redis: true,
183 redis_ttl: Some(ttl),
184 sql: false,
185 state: None,
186 }
187 }
188
189 #[must_use]
194 pub fn durable() -> Self {
195 Self {
196 redis: false,
197 redis_ttl: None,
198 sql: true,
199 state: None,
200 }
201 }
202
203 #[must_use]
213 pub fn with_state(mut self, state: impl Into<String>) -> Self {
214 self.state = Some(state.into());
215 self
216 }
217
218 #[must_use]
220 pub fn stores_anywhere(&self) -> bool {
221 self.redis || self.sql
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Hash)]
231pub struct OptionsKey {
232 pub redis: bool,
234 pub redis_ttl: Option<CacheTtl>,
236 pub sql: bool,
238}
239
240impl From<&SubmitOptions> for OptionsKey {
241 fn from(opts: &SubmitOptions) -> Self {
242 Self {
243 redis: opts.redis,
244 redis_ttl: opts.redis_ttl,
245 sql: opts.sql,
246 }
247 }
248}
249
250impl From<SubmitOptions> for OptionsKey {
251 fn from(opts: SubmitOptions) -> Self {
252 Self::from(&opts)
253 }
254}
255
256impl OptionsKey {
257 #[must_use]
259 pub fn to_options(&self) -> SubmitOptions {
260 SubmitOptions {
261 redis: self.redis,
262 redis_ttl: self.redis_ttl,
263 sql: self.sql,
264 state: None,
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_default_options() {
275 let opts = SubmitOptions::default();
276 assert!(opts.redis);
277 assert!(opts.redis_ttl.is_none());
278 assert!(opts.sql);
279 }
280
281 #[test]
282 fn test_cache_options() {
283 let opts = SubmitOptions::cache(CacheTtl::Hour);
284 assert!(opts.redis);
285 assert_eq!(opts.redis_ttl, Some(CacheTtl::Hour));
286 assert!(!opts.sql);
287 }
288
289 #[test]
290 fn test_durable_options() {
291 let opts = SubmitOptions::durable();
292 assert!(!opts.redis);
293 assert!(opts.sql);
294 }
295
296 #[test]
297 fn test_stores_anywhere() {
298 assert!(SubmitOptions::default().stores_anywhere());
299 assert!(SubmitOptions::cache(CacheTtl::Minute).stores_anywhere());
300 assert!(SubmitOptions::durable().stores_anywhere());
301
302 let nowhere = SubmitOptions {
303 redis: false,
304 sql: false,
305 ..Default::default()
306 };
307 assert!(!nowhere.stores_anywhere());
308 }
309
310 #[test]
311 fn test_options_key_grouping() {
312 let opts1 = SubmitOptions::default();
313 let opts2 = SubmitOptions::default();
314
315 let key1 = OptionsKey::from(&opts1);
316 let key2 = OptionsKey::from(&opts2);
317
318 assert_eq!(key1, key2);
320 }
321
322 #[test]
323 fn test_options_key_same_ttl_enum() {
324 let opts1 = SubmitOptions::cache(CacheTtl::Hour);
326 let opts2 = SubmitOptions::cache(CacheTtl::Hour);
327
328 let key1 = OptionsKey::from(&opts1);
329 let key2 = OptionsKey::from(&opts2);
330
331 assert_eq!(key1, key2);
332 }
333
334 #[test]
335 fn test_options_key_different_ttl_enum() {
336 let opts1 = SubmitOptions::cache(CacheTtl::Hour);
338 let opts2 = SubmitOptions::cache(CacheTtl::Day);
339
340 let key1 = OptionsKey::from(&opts1);
341 let key2 = OptionsKey::from(&opts2);
342
343 assert_ne!(key1, key2);
344 }
345
346 #[test]
347 fn test_cache_ttl_to_duration() {
348 assert_eq!(CacheTtl::Minute.to_duration(), Duration::from_secs(60));
349 assert_eq!(CacheTtl::Short.to_duration(), Duration::from_secs(300));
350 assert_eq!(CacheTtl::Hour.to_duration(), Duration::from_secs(3600));
351 assert_eq!(CacheTtl::Day.to_duration(), Duration::from_secs(86400));
352 assert_eq!(CacheTtl::Custom(123).to_duration(), Duration::from_secs(123));
353 }
354
355 #[test]
356 fn test_cache_ttl_from_duration_snapping() {
357 assert_eq!(CacheTtl::from(Duration::from_secs(45)), CacheTtl::Minute);
359 assert_eq!(CacheTtl::from(Duration::from_secs(90)), CacheTtl::Minute);
360
361 assert_eq!(CacheTtl::from(Duration::from_secs(180)), CacheTtl::Short);
363
364 assert_eq!(CacheTtl::from(Duration::from_secs(3600)), CacheTtl::Hour);
366 }
367
368 #[test]
369 fn test_options_key_roundtrip() {
370 let original = SubmitOptions::cache(CacheTtl::Hour);
371 let key = OptionsKey::from(&original);
372 let recovered = key.to_options();
373
374 assert_eq!(original.redis, recovered.redis);
375 assert_eq!(original.redis_ttl, recovered.redis_ttl);
376 assert_eq!(original.sql, recovered.sql);
377 }
378
379 #[test]
380 fn test_options_key_hashable() {
381 use std::collections::HashMap;
382
383 let mut map: HashMap<OptionsKey, Vec<String>> = HashMap::new();
384
385 let key = OptionsKey::from(&SubmitOptions::default());
386 map.entry(key).or_default().push("item1".into());
387
388 assert_eq!(map.len(), 1);
389 }
390
391 #[test]
392 fn test_state_default_none() {
393 let opts = SubmitOptions::default();
394 assert!(opts.state.is_none());
395 }
396
397 #[test]
398 fn test_state_with_state_builder() {
399 let opts = SubmitOptions::default().with_state("delta");
400 assert_eq!(opts.state, Some("delta".to_string()));
401 }
402
403 #[test]
404 fn test_state_cache_with_state() {
405 let opts = SubmitOptions::cache(CacheTtl::Hour).with_state("pending");
406 assert!(opts.redis);
407 assert!(!opts.sql);
408 assert_eq!(opts.state, Some("pending".to_string()));
409 }
410
411 #[test]
412 fn test_state_durable_with_state() {
413 let opts = SubmitOptions::durable().with_state("archived");
414 assert!(!opts.redis);
415 assert!(opts.sql);
416 assert_eq!(opts.state, Some("archived".to_string()));
417 }
418
419 #[test]
420 fn test_state_to_options_preserves_none() {
421 let opts = SubmitOptions::cache(CacheTtl::Hour);
422 let key = OptionsKey::from(&opts);
423 let recovered = key.to_options();
424 assert!(recovered.state.is_none());
425 }
426}