zero_bounce/utility/structures/
bulk.rs1use 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 return_url: Option<String>,
86 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 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 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 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 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}