switchgear_service_api/
offer.rs

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