sync_engine/
submit_options.rs1use std::time::Duration;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub enum CacheTtl {
51 Minute,
53 Short,
55 Medium,
57 Hour,
59 Day,
61 Week,
63 Custom(u64),
65}
66
67impl CacheTtl {
68 #[must_use]
73 pub fn custom_secs(secs: u64) -> Self {
74 Self::Custom(secs)
75 }
76
77 #[must_use]
79 pub fn to_duration(self) -> Duration {
80 match self {
81 CacheTtl::Minute => Duration::from_secs(60),
82 CacheTtl::Short => Duration::from_secs(5 * 60),
83 CacheTtl::Medium => Duration::from_secs(15 * 60),
84 CacheTtl::Hour => Duration::from_secs(60 * 60),
85 CacheTtl::Day => Duration::from_secs(24 * 60 * 60),
86 CacheTtl::Week => Duration::from_secs(7 * 24 * 60 * 60),
87 CacheTtl::Custom(secs) => Duration::from_secs(secs),
88 }
89 }
90
91 #[must_use]
93 pub fn as_secs(self) -> u64 {
94 self.to_duration().as_secs()
95 }
96}
97
98impl From<Duration> for CacheTtl {
99 fn from(d: Duration) -> Self {
101 let secs = d.as_secs();
102 match secs {
103 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), }
111 }
112}
113
114#[derive(Debug, Clone)]
124pub struct SubmitOptions {
125 pub redis: bool,
129
130 pub redis_ttl: Option<CacheTtl>,
136
137 pub sql: bool,
141
142 pub state: Option<String>,
149}
150
151impl Default for SubmitOptions {
152 fn default() -> Self {
153 Self {
154 redis: true,
155 redis_ttl: None,
156 sql: true,
157 state: None,
158 }
159 }
160}
161
162impl SubmitOptions {
163 #[must_use]
183 pub fn cache(ttl: CacheTtl) -> Self {
184 Self {
185 redis: true,
186 redis_ttl: Some(ttl),
187 sql: false,
188 state: None,
189 }
190 }
191
192 #[must_use]
197 pub fn durable() -> Self {
198 Self {
199 redis: false,
200 redis_ttl: None,
201 sql: true,
202 state: None,
203 }
204 }
205
206 #[must_use]
216 pub fn with_state(mut self, state: impl Into<String>) -> Self {
217 self.state = Some(state.into());
218 self
219 }
220
221 #[must_use]
223 pub fn stores_anywhere(&self) -> bool {
224 self.redis || self.sql
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Hash)]
234pub struct OptionsKey {
235 pub redis: bool,
237 pub redis_ttl: Option<CacheTtl>,
239 pub sql: bool,
241}
242
243impl From<&SubmitOptions> for OptionsKey {
244 fn from(opts: &SubmitOptions) -> Self {
245 Self {
246 redis: opts.redis,
247 redis_ttl: opts.redis_ttl,
248 sql: opts.sql,
249 }
250 }
251}
252
253impl From<SubmitOptions> for OptionsKey {
254 fn from(opts: SubmitOptions) -> Self {
255 Self::from(&opts)
256 }
257}
258
259impl OptionsKey {
260 #[must_use]
262 pub fn to_options(&self) -> SubmitOptions {
263 SubmitOptions {
264 redis: self.redis,
265 redis_ttl: self.redis_ttl,
266 sql: self.sql,
267 state: None,
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_default_options() {
278 let opts = SubmitOptions::default();
279 assert!(opts.redis);
280 assert!(opts.redis_ttl.is_none());
281 assert!(opts.sql);
282 }
283
284 #[test]
285 fn test_cache_options() {
286 let opts = SubmitOptions::cache(CacheTtl::Hour);
287 assert!(opts.redis);
288 assert_eq!(opts.redis_ttl, Some(CacheTtl::Hour));
289 assert!(!opts.sql);
290 }
291
292 #[test]
293 fn test_durable_options() {
294 let opts = SubmitOptions::durable();
295 assert!(!opts.redis);
296 assert!(opts.sql);
297 }
298
299 #[test]
300 fn test_stores_anywhere() {
301 assert!(SubmitOptions::default().stores_anywhere());
302 assert!(SubmitOptions::cache(CacheTtl::Minute).stores_anywhere());
303 assert!(SubmitOptions::durable().stores_anywhere());
304
305 let nowhere = SubmitOptions {
306 redis: false,
307 sql: false,
308 ..Default::default()
309 };
310 assert!(!nowhere.stores_anywhere());
311 }
312
313 #[test]
314 fn test_options_key_grouping() {
315 let opts1 = SubmitOptions::default();
316 let opts2 = SubmitOptions::default();
317
318 let key1 = OptionsKey::from(&opts1);
319 let key2 = OptionsKey::from(&opts2);
320
321 assert_eq!(key1, key2);
323 }
324
325 #[test]
326 fn test_options_key_same_ttl_enum() {
327 let opts1 = SubmitOptions::cache(CacheTtl::Hour);
329 let opts2 = SubmitOptions::cache(CacheTtl::Hour);
330
331 let key1 = OptionsKey::from(&opts1);
332 let key2 = OptionsKey::from(&opts2);
333
334 assert_eq!(key1, key2);
335 }
336
337 #[test]
338 fn test_options_key_different_ttl_enum() {
339 let opts1 = SubmitOptions::cache(CacheTtl::Hour);
341 let opts2 = SubmitOptions::cache(CacheTtl::Day);
342
343 let key1 = OptionsKey::from(&opts1);
344 let key2 = OptionsKey::from(&opts2);
345
346 assert_ne!(key1, key2);
347 }
348
349 #[test]
350 fn test_cache_ttl_to_duration() {
351 assert_eq!(CacheTtl::Minute.to_duration(), Duration::from_secs(60));
352 assert_eq!(CacheTtl::Short.to_duration(), Duration::from_secs(300));
353 assert_eq!(CacheTtl::Hour.to_duration(), Duration::from_secs(3600));
354 assert_eq!(CacheTtl::Day.to_duration(), Duration::from_secs(86400));
355 assert_eq!(CacheTtl::Custom(123).to_duration(), Duration::from_secs(123));
356 }
357
358 #[test]
359 fn test_cache_ttl_from_duration_snapping() {
360 assert_eq!(CacheTtl::from(Duration::from_secs(45)), CacheTtl::Minute);
362 assert_eq!(CacheTtl::from(Duration::from_secs(90)), CacheTtl::Minute);
363
364 assert_eq!(CacheTtl::from(Duration::from_secs(180)), CacheTtl::Short);
366
367 assert_eq!(CacheTtl::from(Duration::from_secs(3600)), CacheTtl::Hour);
369 }
370
371 #[test]
372 fn test_options_key_roundtrip() {
373 let original = SubmitOptions::cache(CacheTtl::Hour);
374 let key = OptionsKey::from(&original);
375 let recovered = key.to_options();
376
377 assert_eq!(original.redis, recovered.redis);
378 assert_eq!(original.redis_ttl, recovered.redis_ttl);
379 assert_eq!(original.sql, recovered.sql);
380 }
381
382 #[test]
383 fn test_options_key_hashable() {
384 use std::collections::HashMap;
385
386 let mut map: HashMap<OptionsKey, Vec<String>> = HashMap::new();
387
388 let key = OptionsKey::from(&SubmitOptions::default());
389 map.entry(key).or_default().push("item1".into());
390
391 assert_eq!(map.len(), 1);
392 }
393
394 #[test]
395 fn test_state_default_none() {
396 let opts = SubmitOptions::default();
397 assert!(opts.state.is_none());
398 }
399
400 #[test]
401 fn test_state_with_state_builder() {
402 let opts = SubmitOptions::default().with_state("delta");
403 assert_eq!(opts.state, Some("delta".to_string()));
404 }
405
406 #[test]
407 fn test_state_cache_with_state() {
408 let opts = SubmitOptions::cache(CacheTtl::Hour).with_state("pending");
409 assert!(opts.redis);
410 assert!(!opts.sql);
411 assert_eq!(opts.state, Some("pending".to_string()));
412 }
413
414 #[test]
415 fn test_state_durable_with_state() {
416 let opts = SubmitOptions::durable().with_state("archived");
417 assert!(!opts.redis);
418 assert!(opts.sql);
419 assert_eq!(opts.state, Some("archived".to_string()));
420 }
421
422 #[test]
423 fn test_state_to_options_preserves_none() {
424 let opts = SubmitOptions::cache(CacheTtl::Hour);
425 let key = OptionsKey::from(&opts);
426 let recovered = key.to_options();
427 assert!(recovered.state.is_none());
428 }
429}