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 ...">