1use crate::service::HasServiceErrorSource;
2use async_trait::async_trait;
3use email_address::EmailAddress;
4use serde::{Deserialize, Serialize};
5use std::error::Error;
6pub use uuid::Uuid;
7
8#[async_trait]
9pub trait OfferStore {
10 type Error: Error + Send + Sync + 'static + HasServiceErrorSource;
11
12 async fn get_offer(
13 &self,
14 partition: &str,
15 id: &Uuid,
16 ) -> Result<Option<OfferRecord>, Self::Error>;
17
18 async fn get_offers(
19 &self,
20 partition: &str,
21 start: usize,
22 count: usize,
23 ) -> Result<Vec<OfferRecord>, Self::Error>;
24
25 async fn post_offer(&self, offer: OfferRecord) -> Result<Option<Uuid>, Self::Error>;
26
27 async fn put_offer(&self, offer: OfferRecord) -> Result<bool, Self::Error>;
28
29 async fn delete_offer(&self, partition: &str, id: &Uuid) -> Result<bool, Self::Error>;
30}
31
32#[async_trait]
33pub trait OfferMetadataStore {
34 type Error: Error + Send + Sync + 'static + HasServiceErrorSource;
35
36 async fn get_metadata(
37 &self,
38 partition: &str,
39 id: &Uuid,
40 ) -> Result<Option<OfferMetadata>, Self::Error>;
41
42 async fn get_all_metadata(
43 &self,
44 partition: &str,
45 start: usize,
46 count: usize,
47 ) -> Result<Vec<OfferMetadata>, Self::Error>;
48
49 async fn post_metadata(&self, offer: OfferMetadata) -> Result<Option<Uuid>, Self::Error>;
50
51 async fn put_metadata(&self, offer: OfferMetadata) -> Result<bool, Self::Error>;
52
53 async fn delete_metadata(&self, partition: &str, id: &Uuid) -> Result<bool, Self::Error>;
54}
55
56#[async_trait]
57pub trait HttpOfferClient: OfferStore + OfferMetadataStore {
58 async fn health(&self) -> Result<(), <Self as OfferStore>::Error>;
59}
60
61#[async_trait]
62pub trait OfferProvider {
63 type Error: Error + Send + Sync + 'static + HasServiceErrorSource;
64
65 async fn offer(
66 &self,
67 hostname: &str,
68 partition: &str,
69 id: &Uuid,
70 ) -> Result<Option<Offer>, Self::Error>;
71}
72
73#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct Offer {
76 pub partition: String,
77 pub id: Uuid,
78 pub max_sendable: u64,
79 pub min_sendable: u64,
80 pub metadata_json_string: String,
81 pub metadata_json_hash: [u8; 32],
82 pub timestamp: chrono::DateTime<chrono::Utc>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub expires: Option<chrono::DateTime<chrono::Utc>>,
85}
86
87impl Offer {
88 pub fn is_expired(&self) -> bool {
89 let now = chrono::Utc::now();
90
91 if now < self.timestamp {
92 return true;
93 }
94
95 if let Some(expires) = self.expires {
96 if now > expires {
97 return true;
98 }
99 }
100
101 false
102 }
103}
104
105#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct OfferRecordSparse {
108 pub max_sendable: u64,
109 pub min_sendable: u64,
110 pub metadata_id: Uuid,
111 pub timestamp: chrono::DateTime<chrono::Utc>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub expires: Option<chrono::DateTime<chrono::Utc>>,
114}
115
116#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct OfferRecord {
119 pub partition: String,
120 pub id: Uuid,
121 #[serde(flatten)]
122 pub offer: OfferRecordSparse,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct OfferMetadataSparse {
128 pub text: String,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub long_text: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub image: Option<OfferMetadataImage>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub identifier: Option<OfferMetadataIdentifier>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct OfferMetadata {
140 pub id: Uuid,
141 pub partition: String,
142 #[serde(flatten)]
143 pub metadata: OfferMetadataSparse,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub enum OfferMetadataImage {
149 #[serde(with = "base64_bytes")]
150 Png(Vec<u8>),
151 #[serde(with = "base64_bytes")]
152 Jpeg(Vec<u8>),
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub enum OfferMetadataIdentifier {
158 Text(EmailAddress),
159 Email(EmailAddress),
160}
161
162mod base64_bytes {
163 use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
164 use base64::Engine;
165 use serde::{de, Deserialize, Deserializer, Serializer};
166
167 pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
168 where
169 S: Serializer,
170 {
171 serializer.serialize_str(&BASE64_STANDARD.encode(bytes))
172 }
173
174 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
175 where
176 D: Deserializer<'de>,
177 {
178 let s = String::deserialize(deserializer)?;
179 BASE64_STANDARD.decode(s).map_err(de::Error::custom)
180 }
181}
182
183#[cfg(test)]
184mod test {
185 use crate::offer::{
186 OfferMetadata, OfferMetadataIdentifier, OfferMetadataImage, OfferMetadataSparse,
187 };
188
189 #[test]
190 fn serialize_offer_metadata_for_services() {
191 let metadata = OfferMetadata {
192 id: Default::default(),
193 partition: "default".to_string(),
194 metadata: OfferMetadataSparse {
195 text: "text".to_string(),
196 long_text: Some("long text".to_string()),
197 image: Some(OfferMetadataImage::Png(vec![0, 1])),
198 identifier: Some(OfferMetadataIdentifier::Email(
199 "email@example.com".parse().unwrap(),
200 )),
201 },
202 };
203
204 let metadata = serde_json::to_string(&metadata).unwrap();
205 assert_eq!(
206 r#"{"id":"00000000-0000-0000-0000-000000000000","partition":"default","text":"text","longText":"long text","image":{"png":"AAE="},"identifier":{"email":"email@example.com"}}"#,
207 metadata.as_str()
208 );
209 }
210
211 #[test]
212 fn deserialize_offer_metadata_for_services() {
213 let metadata_expected = OfferMetadata {
214 id: Default::default(),
215 partition: "default".to_string(),
216 metadata: OfferMetadataSparse {
217 text: "text".to_string(),
218 long_text: Some("long text".to_string()),
219 image: Some(OfferMetadataImage::Png(vec![0, 1])),
220 identifier: Some(OfferMetadataIdentifier::Email(
221 "email@example.com".parse().unwrap(),
222 )),
223 },
224 };
225 let metadata = r#"{"id":"00000000-0000-0000-0000-000000000000","partition":"default","text":"text","longText":"long text","image":{"png":"AAE="},"identifier":{"email":"email@example.com"}}"#;
226 let metadata: OfferMetadata = serde_json::from_str(metadata).unwrap();
227 assert_eq!(metadata_expected, metadata);
228 }
229}