baukit_push/
delivery_claim.rs1use std::num::NonZeroU32;
4
5use chrono::{DateTime, NaiveDate, Utc};
6use uuid::Uuid;
7
8use crate::{PushStoreError, PushStoreFuture, PushValidationError};
9
10pub const MAX_DELIVERY_KIND_LENGTH: usize = 64;
12
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
18pub struct DeliveryKind(String);
19
20impl DeliveryKind {
21 pub fn new(kind: impl Into<String>) -> Result<Self, PushValidationError> {
27 let kind = kind.into();
28 let starts_well = kind
29 .bytes()
30 .next()
31 .is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit());
32 let valid = starts_well
33 && kind.len() <= MAX_DELIVERY_KIND_LENGTH
34 && kind.bytes().all(|byte| {
35 byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"_.-".contains(&byte)
36 });
37 if valid {
38 Ok(Self(kind))
39 } else {
40 Err(PushValidationError::DeliveryKind)
41 }
42 }
43
44 #[must_use]
46 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49}
50
51#[derive(Clone, Debug, Eq, Hash, PartialEq)]
53pub struct DeliveryClaim {
54 pub owner_id: Uuid,
56 pub local_date: NaiveDate,
58 pub kind: DeliveryKind,
60}
61
62impl DeliveryClaim {
63 #[must_use]
65 pub const fn new(owner_id: Uuid, local_date: NaiveDate, kind: DeliveryKind) -> Self {
66 Self {
67 owner_id,
68 local_date,
69 kind,
70 }
71 }
72}
73
74pub trait DeliveryClaimStore: Send + Sync {
82 fn claim(
84 &self,
85 claim: DeliveryClaim,
86 claimed_at: DateTime<Utc>,
87 ) -> PushStoreFuture<'_, Result<bool, PushStoreError>>;
88
89 fn release(&self, claim: DeliveryClaim) -> PushStoreFuture<'_, Result<bool, PushStoreError>>;
91
92 fn purge(
97 &self,
98 before: NaiveDate,
99 limit: NonZeroU32,
100 ) -> PushStoreFuture<'_, Result<u64, PushStoreError>>;
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn a_kind_is_a_short_lowercase_identifier() {
109 for valid in ["training_reminder", "weekly-summary", "daily.v2", "7day"] {
110 assert!(DeliveryKind::new(valid).is_ok(), "{valid}");
111 }
112 assert!(DeliveryKind::new("a".repeat(MAX_DELIVERY_KIND_LENGTH)).is_ok());
113 for invalid in [
114 String::new(),
115 "_leading".to_owned(),
116 "Upper".to_owned(),
117 "with space".to_owned(),
118 "a".repeat(MAX_DELIVERY_KIND_LENGTH + 1),
119 ] {
120 assert_eq!(
121 DeliveryKind::new(invalid),
122 Err(PushValidationError::DeliveryKind)
123 );
124 }
125 }
126}