briteverify_rs/types/bulk.rs
1//! ## BriteVerify Bulk API Types [[ref](https://docs.briteverify.com/#944cd18b-8cad-43c2-9e47-7b1e91ba5935)]
2
3// Standard Library Imports
4use std::{fmt, ops::Deref};
5
6// Third Party Imports
7use chrono::prelude::{DateTime, Utc};
8use http::Uri;
9
10// Crate-Level Imports
11use super::{
12 enums::{BatchState, BulkListDirective, VerificationStatus},
13 single::{AddressVerificationArray, VerificationRequest},
14};
15
16// Conditional Imports
17#[doc(hidden)]
18#[cfg(any(test, tarpaulin, feature = "ci"))]
19#[cfg_attr(any(test, tarpaulin, feature = "ci"), allow(unused_imports))]
20pub use self::foundry::*;
21
22// <editor-fold desc="// Bulk Requests ...">
23
24// <editor-fold desc="// BulkVerificationRequest ...">
25
26/// A request for verification of multiple "contact" records
27#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
28#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
29pub struct BulkVerificationRequest {
30 /// The "contact" records to be verified
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 pub contacts: Vec<VerificationRequest>,
33 /// An (optional) directive for how
34 /// the request should be processed.
35 ///
36 /// For example:
37 /// - "start" -> start processing now
38 /// - "terminate" -> stop processing, if not yet complete
39 #[serde(
40 default,
41 skip_serializing_if = "crate::utils::is_unknown_list_directive"
42 )]
43 pub directive: BulkListDirective,
44}
45
46impl BulkVerificationRequest {
47 /// Create a new `BulkVerificationRequest` for the supplied
48 /// contacts with the (optionally) supplied directive.
49 pub fn new<
50 Contact: Into<VerificationRequest>,
51 Directive: Into<BulkListDirective>,
52 ContactCollection: IntoIterator<Item = Contact>,
53 >(
54 contacts: ContactCollection,
55 directive: Directive,
56 ) -> Self {
57 let contacts: Vec<VerificationRequest> = contacts.into_iter().map(Contact::into).collect();
58
59 let directive: BulkListDirective = directive.into();
60
61 BulkVerificationRequest {
62 contacts,
63 directive,
64 }
65 }
66}
67
68// </editor-fold desc="// BulkVerificationRequest ...">
69
70// </editor-fold desc="// Bulk Requests ...">
71
72// <editor-fold desc="// Bulk Responses ...">
73
74// <editor-fold desc="// BulkListCRUDError ...">
75
76/// An error message returned by the BriteVerify API
77#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
78#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
79pub struct BulkListCRUDError {
80 /// A list's BriteVerify API-issued identifier
81 #[serde(
82 default,
83 skip_serializing_if = "Option::is_none",
84 deserialize_with = "crate::utils::empty_string_is_none"
85 )]
86 pub list_id: Option<String>,
87 /// A status identifier or error code
88 #[serde(
89 default,
90 alias = "code",
91 skip_serializing_if = "BatchState::is_unknown"
92 )]
93 pub status: BatchState,
94 /// A human-oriented message containing
95 /// pertinent information about the data
96 /// in the response
97 #[serde(
98 default,
99 skip_serializing_if = "Option::is_none",
100 deserialize_with = "crate::utils::empty_string_is_none"
101 )]
102 pub message: Option<String>,
103}
104
105// </editor-fold desc="// BulkListCRUDError ...">
106
107// <editor-fold desc="// VerificationListState ...">
108
109/// Details of the current "state" of a bulk verification
110/// job / request / "list" ([ref](https://docs.briteverify.com/#0b5a2a7a-4062-4327-ab0a-4675592e3cd6))
111#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
112#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
113pub struct VerificationListState {
114 /// The list's unique identifier, issued by
115 /// and specific to the BriteVerify API.
116 pub id: String,
117 /// The list's account-specific, user-supplied
118 /// identifier.
119 ///
120 /// ___
121 /// **NOTE:** Lists cannot be uniquely identified
122 /// by this value, as this value exclusively functions
123 /// as a shared point of reference for a specific
124 /// down-stream client of a given user. **If a list
125 /// is _created_ with an external id, it cannot
126 /// be retrieved or referenced without supplying
127 /// the same id as part of the request**.
128 ///
129 /// This field is offered by the
130 /// BriteVerify API as a way to associate an
131 /// identifier with lists that might processed
132 /// on behalf of a user's down-stream clients
133 /// and noted as being ideal for and primarily
134 /// used by agencies or resellers.
135 /// [[ref](https://docs.briteverify.com/#38b4c9eb-31b1-4b8e-9295-a783d8043bc1:~:text=URL%20Parameters-,external_id,-(optional))]
136 /// ___
137 #[serde(
138 default,
139 alias = "account_external_id",
140 skip_serializing_if = "Option::is_none",
141 deserialize_with = "crate::utils::deserialize_ext_id"
142 )]
143 pub external_id: Option<String>,
144 /// The list's current "state" (i.e. its
145 /// current place in the general flow from
146 /// "newly created" to "completely processed")
147 #[serde(default)]
148 pub state: BatchState,
149 /// The number of the list's associated records
150 /// that have been processed, as an integer
151 /// percentage out of 100 (e.g. 10/100 -> 10)
152 #[serde(default)]
153 pub progress: u64,
154 /// The total number of the list's associated
155 /// records that have already been processed
156 #[serde(default)]
157 pub total_verified: u64,
158 /// The list's total number of result "pages"
159 ///
160 /// > **NOTE:** this field will only ever be
161 /// > populated if the list's current state
162 /// > is "completed"
163 #[serde(default)]
164 pub page_count: Option<u64>,
165 /// The total number of "bare" email addresses
166 /// from the list's associated records that have
167 /// already been processed
168 #[serde(default)]
169 pub total_verified_emails: u64,
170 /// The total number of "bare" phone numbers
171 /// from the list's associated records that have
172 /// already been processed
173 #[serde(default)]
174 pub total_verified_phones: u64,
175 /// The timestamp of the list's initial creation
176 ///
177 /// > **NOTE:** the BriteVerify API documentation
178 /// > does not explicitly specify a timezone for
179 /// > these timestamps, but observed behavior seems
180 /// > to indicate they are UTC. Until BriteVerify
181 /// > explicitly states otherwise, `briteverify_rs`
182 /// > will continue to parse all timestamp fields
183 /// > with an assumed timezone of UTC.
184 #[cfg_attr(
185 any(test, tarpaulin, feature = "ci"),
186 serde(serialize_with = "crate::utils::serialize_timestamp")
187 )]
188 #[serde(deserialize_with = "crate::utils::deserialize_timestamp")]
189 pub created_at: DateTime<Utc>,
190 /// The URL at which the list's processed results
191 /// can be retrieved
192 ///
193 /// > **NOTE:** observed behavior indicates that
194 /// > this URL will always point to the *first page*
195 /// > of the list's results. The value of this response's
196 /// > `page_count` field should be referenced when
197 /// > actually *retrieving* results to determine
198 /// > the total number of pages that need to be
199 /// > fetched.
200 #[serde(
201 default,
202 serialize_with = "crate::utils::serialize_uri",
203 deserialize_with = "crate::utils::deserialize_uri"
204 )]
205 pub results_path: Option<Uri>,
206 /// The date/time after which the list's results
207 /// will expire, and will therefore no longer be
208 /// visible / retrievable from the BriteVerify API
209 #[serde(
210 default,
211 deserialize_with = "crate::utils::deserialize_maybe_timestamp"
212 )]
213 pub expiration_date: Option<DateTime<Utc>>,
214 /// A list of error encountered by the BriteVerify API
215 /// while processing the list's associated records
216 #[serde(default = "Vec::new")]
217 pub errors: Vec<BulkListCRUDError>,
218}
219
220// </editor-fold desc="// VerificationListState ...">
221
222// <editor-fold desc="// GetListStatesResponse ...">
223
224/// All bulk verification lists created within
225/// the last 7 calendar days, optionally filtered
226/// by any user-specified parameters (e.g. `date`,
227/// `page`, or `state`)
228#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
229#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
230pub struct GetListStatesResponse {
231 /// Usually page numbers (if provided)
232 #[serde(default)]
233 pub message: Option<String>,
234 /// A list of [`VerificationListState`](VerificationListState)s
235 /// matching any provided filters (defaults to all
236 /// extant lists if no filters are specified).
237 #[serde(default)]
238 pub lists: Vec<VerificationListState>,
239}
240
241impl Deref for GetListStatesResponse {
242 type Target = Vec<VerificationListState>;
243
244 #[cfg_attr(tarpaulin, coverage(off))]
245 fn deref(&self) -> &Self::Target {
246 &self.lists
247 }
248}
249
250impl GetListStatesResponse {
251 /// The `id`s of the collected `VerificationListState`s
252 pub fn ids(&self) -> Vec<&str> {
253 self.lists.iter().map(|list| list.id.as_str()).collect()
254 }
255
256 /// Extract the current page references from
257 /// the response's [`message`](GetListStatesResponse::message)
258 /// field if it is populated.
259 /// ___
260 /// **NOTE:** This implementation is predicated on
261 /// an observed "pattern" in the official response
262 /// examples that shows page-related messages as
263 /// always having the format "Page X of Y".
264 ///
265 /// This method *will* fail in potentially strange
266 /// ways if that ever changes or simply proves to
267 /// be inaccurate. It will not, however, cause a
268 /// panic. It will simply return (1, 1) with no
269 /// regard for the veracity of those values.
270 fn _pages(&self) -> (u64, u64) {
271 match self.message.as_ref() {
272 None => (1u64, 1u64),
273 Some(message) => {
274 let mut values = message
275 .split(' ')
276 .map(|value| value.parse::<u64>())
277 .filter(Result::is_ok)
278 .map(Result::unwrap);
279
280 (values.next().unwrap_or(1u64), values.next().unwrap_or(1u64))
281 }
282 }
283 }
284
285 /// Get the get the current "page" number with
286 /// relative to the total number of list "pages"
287 /// matching the filter criteria that resulted in
288 /// the current response
289 pub fn current_page(&self) -> u64 {
290 self._pages().0
291 }
292
293 /// Get the total number of available list "pages"
294 /// matching the filter criteria that resulted in
295 /// the current response
296 pub fn total_pages(&self) -> u64 {
297 self._pages().1
298 }
299
300 /// Get a specific `VerificationListState` from the collection by `id`
301 pub fn get_list_by_id<ListId: fmt::Display>(
302 &self,
303 list_id: ListId,
304 ) -> Option<&VerificationListState> {
305 let list_id = list_id.to_string();
306
307 self.lists.iter().find(|list| list.id == list_id)
308 }
309}
310
311// </editor-fold desc="// GetListStatesResponse ...">
312
313// <editor-fold desc="// BulkListCRUDResponse ...">
314
315/// The BriteVerify API's response to a valid,
316/// well-formed request to create, update, or
317/// delete a bulk verification list
318#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
319#[derive(Debug, serde::Serialize, serde::Deserialize)]
320pub struct BulkListCRUDResponse {
321 /// The current "status" of the
322 /// created / updated / deleted list
323 #[serde(default, alias = "code")]
324 pub status: BatchState,
325 /// A human-oriented message containing
326 /// pertinent information about the result
327 /// of the requested operation
328 #[serde(default)]
329 pub message: String,
330 /// Details of the associated list's
331 /// current "state"
332 pub list: VerificationListState,
333}
334
335// </editor-fold desc="// BulkListCRUDResponse ...">
336
337/// The BriteVerify API's response to a request to
338/// create a new bulk verification list
339pub type CreateListResponse = BulkListCRUDResponse;
340
341/// The BriteVerify API's response to a request to
342/// mutate an extant bulk verification list
343pub type UpdateListResponse = BulkListCRUDResponse;
344
345/// The BriteVerify API's response to a request
346/// to delete an extant bulk verification list
347pub type DeleteListResponse = BulkListCRUDResponse;
348
349// <editor-fold desc="// BulkEmailVerificationArray ...">
350
351/// The `email` element of a bulk verification result
352/// record returned by the BriteVerify API
353#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
354#[derive(Debug, serde::Serialize, serde::Deserialize)]
355pub struct BulkEmailVerificationArray {
356 /// The verified email address
357 pub email: String,
358 /// The email address's validity "status"
359 /// ([ref](https://knowledge.validity.com/hc/en-us/articles/360047111771-Understanding-Statuses-in-BriteVerify#h_01F79WHSGY6FJ6YN1083JWR3QJ))
360 pub status: VerificationStatus,
361 /// The email address's "secondary" validity status
362 pub secondary_status: Option<String>,
363}
364
365// </editor-fold desc="// BulkEmailVerificationArray ...">
366
367// <editor-fold desc="// BulkPhoneNumberVerificationArray ...">
368
369/// The `phone` element of a bulk verification result
370/// record returned by the BriteVerify API
371#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
372#[derive(Debug, serde::Serialize, serde::Deserialize)]
373pub struct BulkPhoneNumberVerificationArray {
374 /// The verified phone number
375 pub phone: String,
376 /// The phone number's validity "status"
377 /// ([ref](https://knowledge.validity.com/hc/en-us/articles/360047111771-Understanding-Statuses-in-BriteVerify#h_01F79WJXQFFEHWKTJPHPG944NS))
378 pub status: VerificationStatus,
379 /// The geographical area within which
380 /// the phone number was initially registered
381 /// or should be considered "valid"
382 ///
383 /// > **NOTE:** from observed behavior, this
384 /// > field is never *not* `null`
385 pub phone_location: Option<String>,
386 /// The phone number's "secondary" validity status
387 pub secondary_status: Option<String>,
388 /// The "type" of service the phone number
389 /// most likely uses (e.g. "land line", "mobile", etc..)
390 #[serde(rename(serialize = "phone_service_type", deserialize = "phone_service_type"))]
391 pub service_type: Option<String>,
392}
393
394// </editor-fold desc="// BulkPhoneNumberVerificationArray ...">
395
396/// The `address` element of a bulk verification
397/// result record returned by the BriteVerify API
398pub type BulkAddressVerificationArray = AddressVerificationArray;
399
400// <editor-fold desc="// BulkVerificationResult ...">
401
402/// A single result record returned by
403/// the BriteVerify bulk verification API
404/// for "contacts"-type requests
405#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
406#[derive(Debug, serde::Serialize, serde::Deserialize)]
407pub struct BulkContactVerificationResult {
408 /// Verification data for the requested
409 /// email address
410 #[serde(default)]
411 pub email: Option<BulkEmailVerificationArray>,
412 /// Verification data for the requested
413 /// phone number
414 #[serde(default)]
415 pub phone: Option<BulkPhoneNumberVerificationArray>,
416 /// Verification data for the requested
417 /// street address
418 #[serde(default)]
419 pub address: Option<BulkAddressVerificationArray>,
420}
421
422/// A single result record returned by
423/// the BriteVerify bulk verification API
424#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
425#[derive(serde::Serialize, serde::Deserialize)]
426#[serde(untagged)]
427pub enum BulkVerificationResult {
428 /// A single result record returned by
429 /// the BriteVerify bulk verification API
430 /// for "contacts"-type requests
431 Contact(BulkContactVerificationResult),
432 /// A single result record returned by
433 /// the BriteVerify bulk verification API
434 /// for "email"-type requests
435 Email(BulkEmailVerificationArray),
436}
437
438impl fmt::Debug for BulkVerificationResult {
439 #[cfg_attr(tarpaulin, coverage(off))]
440 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
441 match self {
442 Self::Email(response) => fmt::Debug::fmt(response, formatter),
443 Self::Contact(response) => fmt::Debug::fmt(response, formatter),
444 }
445 }
446}
447
448// </editor-fold desc="// BulkVerificationResult ...">
449
450// <editor-fold desc="// BulkVerificationResponse ...">
451
452/// A "page" of result records and associated
453/// metadata returned by the BriteVerify bulk
454/// verification API
455#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
456#[derive(Debug, serde::Serialize, serde::Deserialize)]
457pub struct BulkVerificationResponse {
458 /// The current "status" of the bulk
459 /// verification list
460 #[serde(default)]
461 pub status: BatchState,
462 /// The total number of result "pages"
463 /// associated with the verification list
464 #[serde(default, alias = "num_pages")]
465 pub page_count: u64,
466 /// A "page" of verification result records
467 #[serde(default)]
468 pub results: Vec<BulkVerificationResult>,
469}
470
471// </editor-fold desc="// BulkVerificationResponse ...">
472
473// <editor-fold desc="// Bulk Responses ...">
474
475// <editor-fold desc="// Test Helpers & Factory Implementations ...">
476
477#[doc(hidden)]
478#[cfg(any(test, tarpaulin, feature = "ci"))]
479mod foundry {
480
481 impl<
482 Contact: Into<super::VerificationRequest>,
483 ContactCollection: IntoIterator<Item = Contact>,
484 > From<Option<ContactCollection>> for super::BulkVerificationRequest
485 {
486 #[cfg_attr(tarpaulin, coverage(off))]
487 #[cfg_attr(tarpaulin, tarpaulin::skip)]
488 fn from(value: Option<ContactCollection>) -> Self {
489 match value {
490 None => Self::default(),
491 Some(contacts) => {
492 let contacts = contacts
493 .into_iter()
494 .map(Contact::into)
495 .collect::<Vec<super::VerificationRequest>>();
496
497 Self {
498 contacts,
499 ..Self::default()
500 }
501 }
502 }
503 }
504 }
505}
506
507// </editor-fold desc="// Test Helpers & Factory Implementations ...">
508
509// <editor-fold desc="// I/O-Free Tests ...">
510
511#[cfg(test)]
512mod tests {
513 // Third-Party Dependencies
514 use crate::types::GetListStatesResponse;
515 use pretty_assertions::assert_eq;
516
517 /// Test that the `BulkVerificationRequest`'s
518 /// `new` constructor method behaves as expected
519 #[rstest::rstest]
520 fn test_new_bulk_verification_request() {
521 let req = super::BulkVerificationRequest::new(
522 Vec::<super::VerificationRequest>::new(),
523 Option::<&str>::None,
524 );
525
526 assert!(req.contacts.is_empty());
527 assert_eq!(req.directive, super::BulkListDirective::Unknown);
528 }
529
530 /// Test that the `GetListStatesResponse`'s
531 /// `_pages` utility method behaves as expected
532 #[rstest::rstest]
533 fn test_list_state_pages() {
534 let no_message = GetListStatesResponse::default();
535 let some_message = GetListStatesResponse {
536 message: Some("Page 12 of 345".to_string()),
537 lists: Vec::new(),
538 };
539
540 assert_eq!(
541 (1u64, 1u64),
542 (no_message.current_page(), no_message.total_pages())
543 );
544 assert_eq!(
545 (12u64, 345u64),
546 (some_message.current_page(), some_message.total_pages())
547 );
548 }
549}
550
551// </editor-fold desc="// I/O-Free Tests ...">