Skip to main content

zero_bounce/utility/structures/
bulk.rs

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