zero_bounce/utility/structures/
bulk.rs

1use std::fmt::Debug;
2
3use bytes::Bytes;
4use chrono::{DateTime, FixedOffset};
5use reqwest::blocking::multipart::{Form, Part};
6
7use serde::Deserialize;
8
9use crate::utility::{ZBResult, ZBError};
10use crate::utility::structures::custom_deserialize::deserialize_date_rfc;
11use crate::utility::structures::custom_deserialize::deserialize_percentage_float;
12
13
14#[derive(Clone, Debug, Deserialize)]
15pub struct ZBFileFeedback {
16    pub success: bool,
17    pub message: String,
18    pub file_name: Option<String>,
19    pub file_id: Option<String>,
20}
21
22
23#[derive(Clone, Debug, Deserialize)]
24pub struct ZBFileStatus {
25    pub success: bool,
26    pub file_id: String,
27    pub file_name: String,
28    pub file_status: String,
29    pub error_reason: Option<String>,
30    pub return_url: Option<String>,
31
32    #[serde(deserialize_with="deserialize_date_rfc")]
33    pub upload_date: DateTime<FixedOffset>,
34
35    #[serde(deserialize_with="deserialize_percentage_float")]
36    pub complete_percentage: f32,
37}
38
39pub enum ZBBulkResponse {
40    Content(Bytes),
41    Feedback(ZBFileFeedback),
42}
43
44impl Debug for ZBBulkResponse {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match &self {
47            Self::Content(cnt) => {
48                write!(f, "<ZBBulkResponse::Content | size {}>", cnt.len())
49            },
50            Self::Feedback(feedback) => {
51                write!(f, "<ZBBulkResponse::Feedback | {:#?}>", feedback)
52            },
53        }
54    }
55}
56
57#[derive(Debug, Clone)]
58pub enum ZBFileContentType {
59    FilePath(String),
60    RawContent(Vec<u8>),
61    Empty,
62}
63
64impl ZBFileContentType {
65    pub fn is_empty(&self) -> bool {
66        match self {
67            Self::Empty => true,
68            Self::RawContent(vec) => vec.len() == 0,
69            _ => false,
70        }
71    }
72}
73
74pub struct ZBFile {
75    content_type: ZBFileContentType,
76    has_header_row: bool,
77    remove_duplicate: bool,
78    email_address_column: u32,
79    first_name_column: Option<u32>,
80    last_name_column: Option<u32>,
81    gender_column: Option<u32>,
82    ip_address_column: Option<u32>,
83}
84
85impl Default for ZBFile {
86    fn default() -> Self {
87        ZBFile {
88            content_type: ZBFileContentType::Empty,
89            has_header_row: true,
90            remove_duplicate: false,
91            email_address_column: 1,
92            first_name_column: None,
93            last_name_column: None,
94            gender_column: None,
95            ip_address_column: None,
96        }
97    }
98}
99
100impl ZBFile {
101
102    pub fn from_path(path_to_file: String) -> ZBFile {
103        ZBFile {
104            content_type: ZBFileContentType::FilePath(path_to_file),
105            ..Default::default()
106        }
107    }
108
109    pub fn from_content(content: Vec<u8>) -> ZBFile {
110        ZBFile {
111            content_type: ZBFileContentType::RawContent(content),
112            ..Default::default()
113        }
114    }
115
116    fn file_content_multipart(&self) -> ZBResult<Part> {
117        match self.content_type.clone() {
118            ZBFileContentType::Empty => Err(ZBError::explicit("bulk content cannot be empty")),
119            ZBFileContentType::FilePath(file_path) => Ok(
120                Part::file(file_path.clone())?
121            ),
122            ZBFileContentType::RawContent(value) => Ok(
123                Part::bytes(value.clone())
124                    .file_name("file.csv")
125                    .mime_str("text/csv")?
126            ),
127        }
128    }
129
130    pub fn generate_multipart(&self) -> ZBResult<Form> {
131        let content_part = self.file_content_multipart()?;
132        let mut multipart_form = Form::new()
133            .part("file", content_part)
134            .text("has_header_row", self.has_header_row.to_string())
135            .text("remove_duplicate", self.remove_duplicate.to_string())
136            .text("email_address_column", self.email_address_column.to_string());
137
138        if let Some(amount) = self.first_name_column {
139            multipart_form = multipart_form.text("first_name_column", amount.to_string());
140        }
141        if let Some(amount) = self.last_name_column {
142            multipart_form = multipart_form.text("last_name_column", amount.to_string());
143        }
144        if let Some(amount) = self.gender_column {
145            multipart_form = multipart_form.text("gender_column", amount.to_string());
146        }
147        if let Some(amount) = self.ip_address_column {
148            multipart_form = multipart_form.text("ip_address_column", amount.to_string());
149        }
150
151        Ok(multipart_form)
152    }
153
154    pub fn set_has_header_row(mut self, has_header_row: bool) -> Self {
155        self.has_header_row = has_header_row;
156        self
157    }
158
159    pub fn set_remove_duplicate(mut self, remove_duplicate: bool) -> Self {
160        self.remove_duplicate = remove_duplicate;
161        self
162    }
163
164    pub fn set_email_address_column(mut self, email_address_column: u32) -> Self {
165        self.email_address_column = email_address_column;
166        self
167    }
168
169    pub fn set_first_name_column(mut self, first_name_column: Option<u32>) -> Self {
170        self.first_name_column = first_name_column;
171        self
172    }
173
174    pub fn set_last_name_column(mut self, last_name_column: Option<u32>) -> Self {
175        self.last_name_column = last_name_column;
176        self
177    }
178
179    pub fn set_gender_column(mut self, gender_column: Option<u32>) -> Self {
180        self.gender_column = gender_column;
181        self
182    }
183
184    pub fn set_ip_address_column(mut self, ip_address_column: Option<u32>) -> Self {
185        self.ip_address_column = ip_address_column;
186        self
187    }
188}
189
190
191#[cfg(test)]
192
193mod test {
194    use chrono::{NaiveDate, NaiveTime, NaiveDateTime};
195    use serde_json::{Result as SerdeResult, from_str};
196
197    use super::*;
198    use crate::utility::mock_constants::BULK_VALIDATION_SUBMIT_OK;
199    use crate::utility::mock_constants::BULK_VALIDATION_STATUS_OK;
200    use crate::utility::mock_constants::BULK_VALIDATION_STATUS_DELETED;
201    use crate::utility::mock_constants::BULK_VALIDATION_RESULT_DELETED;
202    use crate::utility::mock_constants::BULK_VALIDATION_DELETE_OK;
203
204
205    #[test]
206    fn test_parsing_file_submit_response_ok() {
207        let validation: SerdeResult<ZBFileFeedback> = from_str(BULK_VALIDATION_SUBMIT_OK);
208        assert!(validation.is_ok());
209
210        let validation_obj = validation.unwrap();
211        assert_eq!(validation_obj.success, true);
212        assert!(validation_obj.file_id.is_some());
213        assert!(validation_obj.file_name.is_some());
214        assert_eq!(validation_obj.message, "File Accepted");
215    }
216
217    #[test]
218    fn test_parse_file_status_response_ok() {
219        let file_status: SerdeResult<ZBFileStatus> = from_str(BULK_VALIDATION_STATUS_OK);
220        assert!(file_status.is_ok());
221
222        let expected_date_time = NaiveDateTime::new(
223            NaiveDate::from_ymd_opt(2023, 4, 26).unwrap(),
224            NaiveTime::from_hms_opt(17, 52, 23).unwrap(),
225        );
226
227        let file_status_obj = file_status.unwrap();
228        assert_eq!(file_status_obj.success, true);
229        assert_eq!(file_status_obj.complete_percentage, 100.);
230        assert_eq!(file_status_obj.upload_date.naive_utc(), expected_date_time);
231        assert!(file_status_obj.return_url.is_some());
232        assert!(file_status_obj.error_reason.is_none());
233    }
234
235    #[test]
236    fn test_parse_file_status_response_deleted() {
237        let file_status: SerdeResult<ZBFileStatus> = from_str(BULK_VALIDATION_STATUS_DELETED);
238        assert!(file_status.is_ok());
239
240        let expected_date_time = NaiveDateTime::new(
241            NaiveDate::from_ymd_opt(2023, 4, 26).unwrap(),
242            NaiveTime::from_hms_opt(17, 52, 23).unwrap(),
243        );
244
245        let file_status_obj = file_status.unwrap();
246        assert_eq!(file_status_obj.success, true);
247        assert_eq!(file_status_obj.complete_percentage, 0.);
248        assert_eq!(file_status_obj.upload_date.naive_utc(), expected_date_time);
249        assert!(file_status_obj.return_url.is_none());
250        assert!(file_status_obj.error_reason.is_some());
251
252    }
253
254    #[test]
255    fn test_parse_file_result_deleted() {
256        let feedback: SerdeResult<ZBFileFeedback> = from_str(BULK_VALIDATION_RESULT_DELETED);
257        assert!(feedback.is_ok());
258
259        let feedback_obj = feedback.unwrap();
260        assert_eq!(feedback_obj.success, false);
261        assert!(feedback_obj.file_id.is_none());
262        assert!(feedback_obj.file_name.is_none());
263    }
264
265    #[test]
266    fn test_parse_file_delete_ok() {
267        let feedback: SerdeResult<ZBFileFeedback> = from_str(BULK_VALIDATION_DELETE_OK);
268        assert!(feedback.is_ok());
269
270        let feedback_obj = feedback.unwrap();
271        assert_eq!(feedback_obj.success, true);
272        assert!(feedback_obj.file_id.is_some());
273        assert!(feedback_obj.file_name.is_some());
274        assert_eq!(feedback_obj.message, "File Deleted");
275    }
276
277}