switchgear_service/api/
offer.rs

1use crate::api::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(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct OfferRecordRest {
128    pub location: String,
129    #[serde(flatten)]
130    pub offer: OfferRecord,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct OfferMetadataSparse {
136    pub text: String,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub long_text: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub image: Option<OfferMetadataImage>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub identifier: Option<OfferMetadataIdentifier>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct OfferMetadata {
148    pub id: Uuid,
149    pub partition: String,
150    #[serde(flatten)]
151    pub metadata: OfferMetadataSparse,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct OfferMetadataRest {
157    pub location: String,
158    #[serde(flatten)]
159    pub metadata: OfferMetadata,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub enum OfferMetadataImage {
165    #[serde(with = "base64_bytes")]
166    Png(Vec<u8>),
167    #[serde(with = "base64_bytes")]
168    Jpeg(Vec<u8>),
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub enum OfferMetadataIdentifier {
174    Text(EmailAddress),
175    Email(EmailAddress),
176}
177
178mod base64_bytes {
179    use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
180    use base64::Engine;
181    use serde::{de, Deserialize, Deserializer, Serializer};
182
183    pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
184    where
185        S: Serializer,
186    {
187        serializer.serialize_str(&BASE64_STANDARD.encode(bytes))
188    }
189
190    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
191    where
192        D: Deserializer<'de>,
193    {
194        let s = String::deserialize(deserializer)?;
195        BASE64_STANDARD.decode(s).map_err(de::Error::custom)
196    }
197}
198
199#[cfg(test)]
200mod test {
201    use crate::api::offer::{
202        OfferMetadata, OfferMetadataIdentifier, OfferMetadataImage, OfferMetadataSparse,
203    };
204
205    #[test]
206    fn serialize_offer_metadata_for_services() {
207        let metadata = OfferMetadata {
208            id: Default::default(),
209            partition: "default".to_string(),
210            metadata: OfferMetadataSparse {
211                text: "text".to_string(),
212                long_text: Some("long text".to_string()),
213                image: Some(OfferMetadataImage::Png(vec![0, 1])),
214                identifier: Some(OfferMetadataIdentifier::Email(
215                    "email@example.com".parse().unwrap(),
216                )),
217            },
218        };
219
220        let metadata = serde_json::to_string(&metadata).unwrap();
221        assert_eq!(
222            r#"{"id":"00000000-0000-0000-0000-000000000000","partition":"default","text":"text","longText":"long text","image":{"png":"AAE="},"identifier":{"email":"email@example.com"}}"#,
223            metadata.as_str()
224        );
225    }
226
227    #[test]
228    fn deserialize_offer_metadata_for_services() {
229        let metadata_expected = OfferMetadata {
230            id: Default::default(),
231            partition: "default".to_string(),
232            metadata: OfferMetadataSparse {
233                text: "text".to_string(),
234                long_text: Some("long text".to_string()),
235                image: Some(OfferMetadataImage::Png(vec![0, 1])),
236                identifier: Some(OfferMetadataIdentifier::Email(
237                    "email@example.com".parse().unwrap(),
238                )),
239            },
240        };
241        let metadata = r#"{"id":"00000000-0000-0000-0000-000000000000","partition":"default","text":"text","longText":"long text","image":{"png":"AAE="},"identifier":{"email":"email@example.com"}}"#;
242        let metadata: OfferMetadata = serde_json::from_str(metadata).unwrap();
243        assert_eq!(metadata_expected, metadata);
244    }
245}