briteverify_rs/types/
single.rs

1//! ## BriteVerify Real-time Single Transaction API Types ([ref](https://docs.briteverify.com/#79e00732-b734-4308-ac7f-820d62dde01f))
2///
3// Standard Library Imports
4use std::time::Duration;
5
6// Third Party Imports
7use anyhow::Result;
8use serde_json::Value;
9
10// Crate-Level Imports
11use super::enums::{VerificationError, VerificationStatus};
12use crate::errors::BriteVerifyTypeError;
13
14// Conditional Imports
15#[cfg(test)]
16#[doc(hidden)]
17#[allow(unused_imports)]
18pub use self::foundry::*;
19
20// <editor-fold desc="// Request Elements ...">
21
22/// A standardized representation of a street address
23#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(Clone))]
24#[derive(Debug, serde::Serialize, serde::Deserialize)]
25pub struct StreetAddressArray {
26    /// The address's street number and name
27    pub address1: String,
28    /// Additional / supplemental delivery information
29    /// (e.g. apartment, suite, or  P.O. box number)
30    #[serde(
31        default,
32        skip_serializing_if = "Option::is_none",
33        deserialize_with = "crate::utils::empty_string_is_none"
34    )]
35    pub address2: Option<String>,
36    /// The address's city or town
37    pub city: String,
38    /// The address's state or province
39    pub state: String,
40    /// The address's ZIP or postal code
41    pub zip: String,
42}
43
44impl StreetAddressArray {
45    /// Build an `StreetAddressArray` incrementally
46    pub fn builder() -> AddressArrayBuilder {
47        AddressArrayBuilder::new()
48    }
49
50    /// Create a new `StreetAddressArray`
51    /// from the supplied values
52    pub fn from_values<Displayable: ToString>(
53        address1: Displayable,
54        address2: Option<Displayable>,
55        city: Displayable,
56        state: Displayable,
57        zip: Displayable,
58    ) -> Self {
59        let (address1, city, state, zip) = (
60            address1.to_string(),
61            city.to_string(),
62            state.to_string(),
63            zip.to_string(),
64        );
65        let address2 = address2.map(|value| value.to_string());
66
67        Self {
68            address1,
69            address2,
70            city,
71            state,
72            zip,
73        }
74    }
75}
76
77/// Incremental builder for `StreetAddressArray`s
78#[derive(Debug, Default)]
79pub struct AddressArrayBuilder {
80    _address1: Option<String>,
81    _address2: Option<String>,
82    _city: Option<String>,
83    _state: Option<String>,
84    _zip: Option<String>,
85}
86
87impl AddressArrayBuilder {
88    /// Create a new `AddressArrayBuilder`
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// Build a `StreetAddressArray` from the configured values
94    pub fn build(self) -> Result<StreetAddressArray, BriteVerifyTypeError> {
95        if !self.buildable() {
96            Err(BriteVerifyTypeError::UnbuildableAddressArray(Box::new(
97                self,
98            )))
99        } else {
100            Ok(StreetAddressArray::from_values(
101                self._address1.unwrap(),
102                self._address2,
103                self._city.unwrap(),
104                self._state.unwrap(),
105                self._zip.unwrap(),
106            ))
107        }
108    }
109
110    /// Determine if a valid `StreetAddressArray` can be
111    /// constructed from the current builder state
112    pub fn buildable(&self) -> bool {
113        self._address1
114            .as_ref()
115            .is_some_and(|value| !value.trim().is_empty())
116            && self
117                ._city
118                .as_ref()
119                .is_some_and(|value| !value.trim().is_empty())
120            && self
121                ._state
122                .as_ref()
123                .is_some_and(|value| !value.trim().is_empty())
124            && self
125                ._zip
126                .as_ref()
127                .is_some_and(|value| !value.trim().is_empty())
128    }
129
130    /// Set the "zip" value of the
131    /// `StreetAddressArray` being built
132    pub fn zip<Displayable: ToString>(mut self, value: Displayable) -> Self {
133        self._zip = Some(value.to_string());
134        self
135    }
136
137    /// Set the "city" value of the
138    /// `StreetAddressArray` being built
139    pub fn city<Displayable: ToString>(mut self, value: Displayable) -> Self {
140        self._city = Some(value.to_string());
141        self
142    }
143
144    /// Set the "state" value of the
145    /// `StreetAddressArray` being built
146    pub fn state<Displayable: ToString>(mut self, value: Displayable) -> Self {
147        self._state = Some(value.to_string());
148        self
149    }
150
151    /// Set the "address1" value of the
152    /// `StreetAddressArray` being built
153    pub fn address1<Displayable: ToString>(mut self, value: Displayable) -> Self {
154        self._address1 = Some(value.to_string());
155        self
156    }
157
158    /// Set the "address2" value of the
159    /// `StreetAddressArray` being built
160    pub fn address2<Displayable: ToString>(mut self, value: Displayable) -> Self {
161        self._address2 = Some(value.to_string());
162        self
163    }
164
165    /// Create a new `StreetAddressArray` instance
166    /// pre-populated with the supplied argument values
167    pub fn from_values<
168        AddressLine1: ToString,
169        AddressLine2: ToString,
170        CityName: ToString,
171        StateNameOrAbbr: ToString,
172        ZipCode: ToString,
173    >(
174        address1: Option<AddressLine1>,
175        address2: Option<AddressLine2>,
176        city: Option<CityName>,
177        state: Option<StateNameOrAbbr>,
178        zip: Option<ZipCode>,
179    ) -> Self {
180        let mut instance = Self::new();
181
182        if let Some(value) = zip {
183            instance = instance.zip(value);
184        }
185
186        if let Some(value) = city {
187            instance = instance.city(value);
188        }
189
190        if let Some(value) = state {
191            instance = instance.state(value);
192        }
193
194        if let Some(value) = address1 {
195            instance = instance.address1(value);
196        }
197
198        if let Some(value) = address2 {
199            instance = instance.address2(value);
200        }
201
202        instance
203    }
204}
205
206#[cfg(any(test, tarpaulin, feature = "ci"))]
207impl PartialEq for StreetAddressArray {
208    fn eq(&self, other: &Self) -> bool {
209        if self.address2.is_none() != other.address2.is_none() {
210            return false;
211        }
212
213        let (self_addr2, other_addr2) = (
214            self.address2
215                .as_ref()
216                .map_or(String::new(), |val| val.to_string()),
217            other
218                .address2
219                .as_ref()
220                .map_or(String::new(), |val| val.to_string()),
221        );
222
223        crate::utils::caseless_eq(&self.address1, &other.address1)
224            && crate::utils::caseless_eq(&self_addr2, &other_addr2)
225            && crate::utils::caseless_eq(&self.city, &other.city)
226            && crate::utils::caseless_eq(&self.state, &other.state)
227            && crate::utils::caseless_eq(&self.zip, &other.zip)
228    }
229}
230
231// </editor-fold desc="// Request Elements ...">
232
233// <editor-fold desc="// Single-Transaction Requests ...">
234
235/// Request for verification made to one of the BriteVerify
236/// API's single-transaction, real-time endpoints
237#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
238#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
239pub struct VerificationRequest {
240    /// The email address to be verified
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub email: Option<String>,
243    /// The phone number to be verified
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub phone: Option<String>,
246    /// The street address to be verified
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub address: Option<StreetAddressArray>,
249}
250
251impl VerificationRequest {
252    /// Get an builder instance that can be used
253    /// to build up a `VerificationRequest` incrementally
254    pub fn builder() -> VerificationRequestBuilder {
255        VerificationRequestBuilder::new()
256    }
257
258    /// Create a new `VerificationRequest`
259    /// instance from the supplied values
260    pub fn from_values<
261        EmailAddress: ToString,
262        PhoneNumber: ToString,
263        AddressLine1: ToString,
264        AddressLine2: ToString,
265        CityName: ToString,
266        StateNameOrAbbr: ToString,
267        ZipCode: ToString,
268    >(
269        email: Option<EmailAddress>,
270        phone: Option<PhoneNumber>,
271        address1: Option<AddressLine1>,
272        address2: Option<AddressLine2>,
273        city: Option<CityName>,
274        state: Option<StateNameOrAbbr>,
275        zip: Option<ZipCode>,
276    ) -> Result<Self, BriteVerifyTypeError> {
277        VerificationRequestBuilder::from_values(email, phone, address1, address2, city, state, zip)
278            .build()
279    }
280}
281
282impl TryFrom<String> for VerificationRequest {
283    type Error = BriteVerifyTypeError;
284
285    fn try_from(value: String) -> Result<Self, Self::Error> {
286        Self::try_from(value.as_str())
287    }
288}
289
290impl TryFrom<&'_ str> for VerificationRequest {
291    type Error = BriteVerifyTypeError;
292
293    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
294        if let Ok(request) = serde_json::from_str::<VerificationRequest>(value) {
295            return Ok(request);
296        }
297
298        if value.contains('@') {
299            return Ok(Self {
300                email: Some(value.to_string()),
301                ..Self::default()
302            });
303        }
304
305        const PHONE_CHARS: &str = "0123456789 +().- ext";
306
307        if value
308            .to_ascii_lowercase()
309            .chars()
310            .all(|ch| PHONE_CHARS.contains(ch))
311        {
312            return Ok(Self {
313                phone: Some(value.to_string()),
314                ..Self::default()
315            });
316        }
317
318        Err(BriteVerifyTypeError::AmbiguousTryFromValue(
319            value.to_string(),
320        ))
321    }
322}
323
324/// Incremental builder for `VerificationRequest`s
325#[derive(Debug, Default)]
326pub struct VerificationRequestBuilder {
327    _email: Option<String>,
328    _phone: Option<String>,
329    _address: AddressArrayBuilder,
330}
331
332impl VerificationRequestBuilder {
333    /// Create a new `VerificationRequestBuilder` instance
334    pub fn new() -> VerificationRequestBuilder {
335        Self::default()
336    }
337
338    /// Build a `VerificationRequest` from the current
339    /// builder state
340    pub fn build(self) -> Result<VerificationRequest, BriteVerifyTypeError> {
341        if self._email.is_some() || self._phone.is_some() || self._address.buildable() {
342            Ok(VerificationRequest {
343                email: self._email,
344                phone: self._phone,
345                address: self._address.build().ok(),
346            })
347        } else {
348            Err(BriteVerifyTypeError::UnbuildableRequest(Box::new(self)))
349        }
350    }
351
352    /// Set the "email" value of the
353    /// `VerificationRequest` being built
354    pub fn email<Displayable: ToString>(mut self, value: Displayable) -> Self {
355        self._email = Some(value.to_string());
356        self
357    }
358
359    /// Set the "phone" value of the
360    /// `VerificationRequest` being built
361    pub fn phone<Displayable: ToString>(mut self, value: Displayable) -> Self {
362        self._phone = Some(value.to_string());
363        self
364    }
365
366    /// Set the `address.zip` field of the
367    /// `VerificationRequest` being built
368    pub fn zip<Displayable: ToString>(mut self, value: Displayable) -> Self {
369        self._address = self._address.zip(value);
370        self
371    }
372
373    /// Set the `address.city` value of the
374    /// `VerificationRequest` being built
375    pub fn city<Displayable: ToString>(mut self, value: Displayable) -> Self {
376        self._address = self._address.city(value);
377        self
378    }
379
380    /// Set the `address.state` value of the
381    /// `VerificationRequest` being built
382    pub fn state<Displayable: ToString>(mut self, value: Displayable) -> Self {
383        self._address = self._address.state(value);
384        self
385    }
386
387    /// Set the `address.address1` value of the
388    /// `VerificationRequest` being built
389    pub fn address1<Displayable: ToString>(mut self, value: Displayable) -> Self {
390        self._address = self._address.address1(value);
391        self
392    }
393
394    /// Set the `address.address2` value of the
395    /// `VerificationRequest` being built
396    pub fn address2<Displayable: ToString>(mut self, value: Displayable) -> Self {
397        self._address = self._address.address2(value);
398        self
399    }
400
401    /// Determine if a valid `VerificationRequest` can be
402    /// constructed from the current builder state
403    pub fn buildable(&self) -> bool {
404        self._email.is_some() || self._phone.is_some() || self._address.buildable()
405    }
406
407    /// Create a new `VerificationRequestBuilder` instance
408    /// pre-populated with the supplied argument values
409    pub fn from_values<
410        EmailAddress: ToString,
411        PhoneNumber: ToString,
412        AddressLine1: ToString,
413        AddressLine2: ToString,
414        CityName: ToString,
415        StateNameOrAbbr: ToString,
416        ZipCode: ToString,
417    >(
418        email: Option<EmailAddress>,
419        phone: Option<PhoneNumber>,
420        address1: Option<AddressLine1>,
421        address2: Option<AddressLine2>,
422        city: Option<CityName>,
423        state: Option<StateNameOrAbbr>,
424        zip: Option<ZipCode>,
425    ) -> Self {
426        let mut instance = Self {
427            _address: AddressArrayBuilder::from_values(address1, address2, city, state, zip),
428            ..Self::default()
429        };
430
431        if let Some(value) = email {
432            instance = instance.email(value);
433        }
434
435        if let Some(value) = phone {
436            instance = instance.phone(value);
437        }
438
439        instance
440    }
441}
442
443// </editor-fold desc="// Single-Transaction Requests ...">
444
445// <editor-fold desc="// Response Elements ...">
446
447/// The `email` element of a verification response
448#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
449#[derive(Debug, serde::Serialize, serde::Deserialize)]
450pub struct EmailVerificationArray {
451    /// The full (original) [IETF RFC 532](https://www.rfc-editor.org/rfc/rfc5322)
452    /// compliant email address
453    pub address: String,
454    /// The "account" portion of the
455    /// verified email address
456    pub account: String,
457    /// The "domain" portion of the
458    /// verified email address
459    pub domain: String,
460    /// The validity "status" of the
461    /// supplied email address
462    /// [[ref](https://knowledge.validity.com/hc/en-us/articles/360047111771-Understanding-Statuses-in-BriteVerify#h_01F79WHSGY6FJ6YN1083JWR3QJ)]
463    pub status: VerificationStatus,
464    /// The BriteVerify API docs don't provide
465    /// any insight as to what the actual type
466    /// of `connected` might be other than to
467    /// notate that the "usual" value is `null`,
468    /// which `briteverify-rs` interprets as
469    /// "whatever it its, it's nullable".
470    pub connected: Option<Value>,
471    /// Boolean flag indicating the whether
472    /// or not the verified email address should
473    /// be regarded as effectively ephemeral
474    pub disposable: bool,
475    /// Boolean flag indicating the whether
476    /// or not the verified email address
477    /// belongs to a "role" within an
478    /// organization instead of belonging
479    /// directly to a specific human
480    pub role_address: bool,
481    /// The "formal" code representing any
482    /// error(s) encountered by the BriteVerify
483    /// API while verifying the email address
484    /// [[ref](https://knowledge.validity.com/hc/en-us/articles/360047111771-Understanding-Statuses-in-BriteVerify)]
485    #[serde(default, skip_serializing_if = "Option::is_none")]
486    pub error_code: Option<VerificationError>,
487    /// The human-readable form of the response's
488    /// associated "formal" error code
489    #[serde(default, skip_serializing_if = "Option::is_none")]
490    pub error: Option<String>,
491}
492
493/// The `phone` element of a verification response
494#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
495#[derive(Debug, serde::Serialize, serde::Deserialize)]
496pub struct PhoneNumberVerificationArray {
497    /// The phone number from the originating
498    /// verification request
499    ///
500    /// > **NOTE:** from observed behavior, this
501    /// > field will be the requested phone
502    /// > number, scrubbed of all non-numeric
503    /// > characters and formatted as:
504    /// > `"{country_code}{phone_number}"`
505    pub number: String,
506    /// The validity "status" of the
507    /// supplied phone number
508    /// ([ref](https://knowledge.validity.com/hc/en-us/articles/360047111771-Understanding-Statuses-in-BriteVerify#h_01F79WJXQFFEHWKTJPHPG944NS))
509    pub status: VerificationStatus,
510    /// The "type" of service the phone number
511    /// most likely uses (e.g. "land line", "mobile", etc..)
512    #[serde(default)]
513    pub service_type: Option<String>,
514    /// The geographical area within which
515    /// the phone number was initially registered
516    /// or should be considered "valid"
517    ///
518    /// > **NOTE:** from observed behavior, this
519    /// > field is never *not* `null`
520    pub phone_location: Option<Value>,
521    /// A list of errors that were encountered
522    /// while fulfilling the verification request
523    pub errors: Vec<Value>,
524}
525
526/// The `address` element of a verification response
527#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
528#[derive(Debug, serde::Serialize, serde::Deserialize)]
529pub struct AddressVerificationArray {
530    /// The verified address's street number and name
531    pub address1: String,
532    /// Additional / supplemental delivery information
533    /// (e.g. apartment, suite, or  P.O. box number)
534    ///
535    /// > **NOTE:** from observed behavior, this field is
536    /// > always `null`, with the value from the original
537    /// > request sanitized, formatted, and appended to
538    /// > the value of `address1`. For example:
539    /// > `request`:
540    /// > ```yaml
541    /// > {
542    /// >   "address1": "123 Main Street",
543    /// >   "address2": "Suite 100",
544    /// >   # ...
545    /// > }
546    /// > ```
547    /// > `response`:
548    /// > ```yaml
549    /// > {
550    /// >   "address1": "123 Main St Ste 100",
551    /// >   "address2": null,
552    /// >   # ...
553    /// > }
554    /// > ```
555    #[serde(deserialize_with = "crate::utils::empty_string_is_none")]
556    pub address2: Option<String>,
557    /// The verified address's city or town
558    pub city: String,
559    /// The verified address's state or province
560    pub state: String,
561    /// The verified address's ZIP or postal code
562    pub zip: String,
563    /// The validity "status" of the
564    /// supplied street address
565    /// ([ref](https://knowledge.validity.com/hc/en-us/articles/360047111771-Understanding-Statuses-in-BriteVerify#h_01F79WK70K5Z127DYC590TK7PT))
566    pub status: VerificationStatus,
567    /// Boolean flag indicating whether or not
568    /// the supplied address was mutated by
569    /// the BriteVerify API in the process of
570    /// fulfilling the verification request.
571    ///
572    /// > **NOTE:** The BriteVerify API *will* mutate
573    /// > street address during validation in order
574    /// > to sanitize or "standardize" them. The
575    /// > BriteVerify API refers to this mutation as
576    /// > "correction".
577    #[serde(deserialize_with = "crate::utils::deserialize_boolean")]
578    pub corrected: bool,
579    /// A list of errors that were encountered
580    /// while fulfilling the verification request
581    #[serde(default = "Vec::new")]
582    pub errors: Vec<Value>,
583    /// The "secondary" validity status
584    /// of the supplied street address
585    /// ([ref](https://knowledge.validity.com/hc/en-us/articles/360047111771-Understanding-Statuses-in-BriteVerify#:~:text=Secondary%20Statuses-,Secondary%20Status,-Explanation)).
586    ///
587    /// > **NOTE:** from observed behavior, this field does
588    /// > not appear in responses from the BriteVerify
589    /// > API's single-transaction real-time endpoints.
590    /// > It *does* appear in responses from the bulk
591    /// > endpoints, but doesn't appear to do so with
592    /// > appreciable frequency
593    pub secondary_status: Option<String>,
594}
595
596// </editor-fold desc="// Response Elements ...">
597
598// <editor-fold desc="// Single-Transaction Responses ...">
599
600/// A response returned by one of the BriteVerify
601/// API's single-transaction, real-time endpoints
602#[cfg_attr(any(test, tarpaulin, feature = "ci"), derive(PartialEq))]
603#[derive(Debug, serde::Serialize, serde::Deserialize)]
604pub struct VerificationResponse {
605    /// Verification data for the requested
606    /// email address
607    #[serde(default)]
608    pub email: Option<EmailVerificationArray>,
609    /// Verification data for the requested
610    /// phone number
611    #[serde(default)]
612    pub phone: Option<PhoneNumberVerificationArray>,
613    /// Verification data for the requested
614    /// street address
615    #[serde(default)]
616    pub address: Option<AddressVerificationArray>,
617    #[serde(
618        serialize_with = "crate::utils::duration_to_float",
619        deserialize_with = "crate::utils::float_to_duration"
620    )]
621    /// How long (in seconds) the BriteVerify
622    /// API took (internally) to fulfill the
623    /// originating verification request
624    pub duration: Duration,
625}
626
627// </editor-fold desc="// Single-Transaction Responses ...">
628
629// <editor-fold desc="// Test Helpers & Factory Implementations ...">
630
631#[cfg(test)]
632#[doc(hidden)]
633mod foundry {
634    // Standard Library Imports
635    use std::collections::HashMap;
636
637    // Third Party Imports
638    use serde::de::Error;
639    use serde_json::{Map as JsonMap, Value};
640
641    type RawAddressMap = HashMap<String, Option<String>>;
642    type RawAddressJson = JsonMap<String, Value>;
643
644    impl TryFrom<Value> for super::StreetAddressArray {
645        type Error = serde_json::Error;
646
647        fn try_from(value: Value) -> Result<Self, Self::Error> {
648            (&value).try_into()
649        }
650    }
651
652    impl TryFrom<&Value> for super::StreetAddressArray {
653        type Error = serde_json::Error;
654
655        fn try_from(value: &Value) -> Result<Self, Self::Error> {
656            match value.as_object() {
657                None => Err(Self::Error::custom(format!(
658                    "Cannot create a `StreetAddressArray` from: {:#?}",
659                    value.as_str()
660                ))),
661                Some(data) => {
662                    if let Some(obj) = data.get("address") {
663                        return obj.try_into();
664                    }
665
666                    data.try_into()
667                }
668            }
669        }
670    }
671
672    impl TryFrom<RawAddressMap> for super::StreetAddressArray {
673        type Error = serde_json::Error;
674
675        fn try_from(data: RawAddressMap) -> Result<Self, Self::Error> {
676            (&data).try_into()
677        }
678    }
679
680    impl TryFrom<&RawAddressMap> for super::StreetAddressArray {
681        type Error = serde_json::Error;
682
683        fn try_from(data: &RawAddressMap) -> Result<Self, Self::Error> {
684            let (address1, address2, city, state, zip) = (
685                data.get("address1").unwrap().clone(),
686                data.get("address2").unwrap().clone(),
687                data.get("city").unwrap().clone(),
688                data.get("state").unwrap().clone(),
689                data.get("zip").unwrap().clone(),
690            );
691
692            match super::AddressArrayBuilder::from_values(address1, address2, city, state, zip)
693                .build()
694            {
695                Ok(address) => Ok(address),
696                Err(_) => Err(Self::Error::custom(format!(
697                    "One or more required fields missing from: {data:#?}"
698                ))),
699            }
700        }
701    }
702
703    impl TryFrom<RawAddressJson> for super::StreetAddressArray {
704        type Error = serde_json::Error;
705
706        fn try_from(value: RawAddressJson) -> Result<Self, Self::Error> {
707            (&value).try_into()
708        }
709    }
710
711    impl TryFrom<&RawAddressJson> for super::StreetAddressArray {
712        type Error = serde_json::Error;
713
714        fn try_from(data: &RawAddressJson) -> Result<Self, Self::Error> {
715            ["address1", "city", "state", "zip"]
716                .into_iter()
717                .map(|key| (key.to_string(), data.get(key)))
718                .map(|(key, value)| (key, value.map(|value| value.to_string())))
719                .collect::<HashMap<String, Option<String>>>()
720                .try_into()
721        }
722    }
723
724    impl super::AddressArrayBuilder {
725        #[cfg_attr(tarpaulin, coverage(off))]
726        #[cfg_attr(tarpaulin, tarpaulin::skip)]
727        /// The current value of builder's `_address1` field
728        pub fn address1_value(&self) -> Option<&String> {
729            self._address1.as_ref()
730        }
731
732        #[cfg_attr(tarpaulin, coverage(off))]
733        #[cfg_attr(tarpaulin, tarpaulin::skip)]
734        /// The current value of builder's `_address2` field
735        pub fn address2_value(&self) -> Option<&String> {
736            self._address2.as_ref()
737        }
738
739        #[cfg_attr(tarpaulin, coverage(off))]
740        #[cfg_attr(tarpaulin, tarpaulin::skip)]
741        /// The current value of builder's `_city1` field
742        pub fn city_value(&self) -> Option<&String> {
743            self._city.as_ref()
744        }
745
746        #[cfg_attr(tarpaulin, coverage(off))]
747        #[cfg_attr(tarpaulin, tarpaulin::skip)]
748        /// The current value of builder's `_state` field
749        pub fn state_value(&self) -> Option<&String> {
750            self._state.as_ref()
751        }
752
753        #[cfg_attr(tarpaulin, coverage(off))]
754        #[cfg_attr(tarpaulin, tarpaulin::skip)]
755        /// The current value of builder's `_zip` field
756        pub fn zip_value(&self) -> Option<&String> {
757            self._zip.as_ref()
758        }
759    }
760}
761
762// </editor-fold desc="// Test Helpers & Factory Implementations ...">
763
764// <editor-fold desc="// I/O-Free Tests ...">
765
766#[cfg(test)]
767mod tests {
768    // Standard-Library Imports
769    use std::clone::Clone;
770
771    // Third-Party Dependencies
772    use anyhow::Result;
773    use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
774
775    // <editor-fold desc="// Constants ...">
776
777    const STATE: &str = "CA";
778    const ZIP: &str = "90210";
779    const CITY: &str = "Any Town";
780    const ADDRESS1: &str = "123 Main St.";
781    const ADDRESS2: Option<&str> = Some("P.O. Box 456");
782    const EMAIL: &str = "test@example.com";
783    const PHONE: &str = "+1 (954) 555-1234 ext. 6789";
784
785    // </editor-fold desc="// Constants ...">
786
787    /// Test that the `AddressArrayBuilder` builds the expected
788    /// `StreetAddressArray` from discrete values
789    #[rstest::rstest]
790    fn test_address_from_values() {
791        let instance = super::AddressArrayBuilder::from_values(
792            Some(ADDRESS1),
793            ADDRESS2,
794            Some(CITY),
795            Some(STATE),
796            Some(ZIP),
797        )
798        .build();
799
800        assert!(instance.is_ok(), "{:#?}", instance.unwrap_err());
801
802        let instance = instance.unwrap();
803
804        assert_str_eq!(ZIP, instance.zip);
805        assert_str_eq!(CITY, instance.city);
806        assert_str_eq!(STATE, instance.state);
807        assert_str_eq!(ADDRESS1, instance.address1);
808        assert_str_eq!(format!("{ADDRESS2:?}"), format!("{:?}", instance.address2));
809    }
810
811    /// Test that `StreetAddressArray`s can be compared
812    /// for equality while the test suite is active
813    #[rstest::rstest]
814    fn test_address_equality() -> Result<()> {
815        let left = super::StreetAddressArray::from_values(ADDRESS1, ADDRESS2, CITY, STATE, ZIP);
816
817        #[allow(clippy::redundant_clone)]
818        let mut right = left.clone();
819
820        assert_eq!(left, right);
821
822        right.address2 = None;
823
824        Ok(assert_ne!(left, right))
825    }
826
827    /// Test that the `AddressArrayBuilder` refuses
828    /// to build "incomplete" `StreetAddressArray`s
829    #[rstest::rstest]
830    fn test_address_buildability() {
831        let builder = super::StreetAddressArray::builder();
832
833        assert_eq!(
834            !builder.buildable(),
835            builder.address1_value().is_none()
836                && builder.address2_value().is_none()
837                && builder.city_value().is_none()
838                && builder.state_value().is_none()
839                && builder.zip_value().is_none()
840        );
841
842        // Addresses cannot be built unless all fields (except
843        // `address2`) contain non-`None`, non-empty values
844        let builder = builder
845            .address2(ADDRESS2.unwrap())
846            .city(CITY)
847            .state(STATE)
848            .zip(ZIP)
849            .build();
850
851        assert!(builder.is_err());
852
853        let builder = match builder.unwrap_err() {
854            super::BriteVerifyTypeError::UnbuildableAddressArray(inner) => inner,
855            _ => panic!(),
856        };
857
858        let builder = builder.address1(ADDRESS1).build();
859
860        assert!(builder.is_ok());
861    }
862
863    /// Test that `VerificationRequest`s can be
864    /// (fallibly) created from "bare" strings
865    #[rstest::rstest]
866    fn test_try_into_verification_request() {
867        assert!(super::VerificationRequest::try_from(EMAIL)
868            .is_ok_and(|req| req.email.is_some_and(|email| email == EMAIL)));
869        assert!(super::VerificationRequest::try_from(PHONE)
870            .is_ok_and(|req| req.phone.is_some_and(|phone| phone == PHONE)));
871
872        let address_data = format!(
873            r#"{{"address":{{"address1":"{ADDRESS1}","city":"{CITY}","state":"{STATE}","zip":"{ZIP}"}}}}"#
874        );
875        assert!(
876            super::VerificationRequest::try_from(address_data).is_ok_and(|req| req.email.is_none()
877                && req.phone.is_none()
878                && req.address.is_some())
879        );
880
881        assert!(super::VerificationRequest::try_from(format!(
882            r#"{ADDRESS1}, {CITY}, {STATE} {ZIP}"#
883        ))
884        .is_err_and(|error| {
885            matches!(error, super::BriteVerifyTypeError::AmbiguousTryFromValue(_))
886        }));
887    }
888
889    /// Test that `VerificationRequestBuilder`s properly
890    /// enforce the non-empty field requirements for each
891    /// buildable request type
892    #[rstest::rstest]
893    fn test_verification_request_buildability() {
894        let builder = super::VerificationRequest::builder();
895
896        assert!(!builder.buildable());
897
898        let builder = builder
899            .address2(ADDRESS2.unwrap())
900            .city(CITY)
901            .state(STATE)
902            .zip(ZIP);
903
904        assert!(!builder.buildable());
905
906        let builder = builder.email(EMAIL).phone(PHONE).address1(ADDRESS1);
907
908        assert!(builder.buildable());
909        assert!(builder.build().is_ok());
910
911        // Unbuildable Request
912        let mut build_result = super::VerificationRequest::builder().build();
913
914        assert!(
915            build_result.as_ref().is_err_and(|error| matches!(
916                error,
917                super::BriteVerifyTypeError::UnbuildableRequest(_)
918            )),
919            "Expected Err(UnbuildableRequest), got: {:#?}",
920            build_result.as_ref(),
921        );
922
923        // Email Request
924        build_result = super::VerificationRequest::builder().email(EMAIL).build();
925
926        assert!(
927            build_result.as_ref().is_ok_and(|req| req.email.is_some()
928                && req.phone.is_none()
929                && req.address.is_none()),
930            "Expected Ok(VerificationRequest) w/ Some(email), got: {:#?}",
931            build_result.as_ref(),
932        );
933
934        // Phone Request
935        build_result = super::VerificationRequest::builder().phone(PHONE).build();
936
937        assert!(
938            build_result.as_ref().is_ok_and(|req| req.email.is_none()
939                && req.phone.is_some()
940                && req.address.is_none()),
941            "Expected Ok(VerificationRequest) w/ Some(phone), got: {:#?}",
942            build_result.as_ref(),
943        );
944
945        // Address Request
946        build_result = super::VerificationRequest::builder()
947            .address1(ADDRESS1)
948            .address2(ADDRESS2.unwrap())
949            .city(CITY)
950            .state(STATE)
951            .zip(ZIP)
952            .build();
953
954        assert!(
955            build_result.as_ref().is_ok_and(|req| req.email.is_none()
956                && req.phone.is_none()
957                && req.address.is_some()),
958            "Expected Ok(VerificationRequest) w/ Some(address), got: {:#?}",
959            build_result.as_ref(),
960        );
961
962        // Email & Phone Request
963        build_result = super::VerificationRequest::builder()
964            .email(EMAIL)
965            .phone(PHONE)
966            .build();
967
968        assert!(
969            build_result.as_ref().is_ok_and(|req| req.email.is_some()
970                && req.phone.is_some()
971                && req.address.is_none()),
972            "Expected Ok(VerificationRequest) w/ Some(email) & Some(phone), got: {:#?}",
973            build_result.as_ref(),
974        );
975
976        // Email & Address Request
977        build_result = super::VerificationRequest::builder()
978            .email(EMAIL)
979            .address1(ADDRESS1)
980            .address2(ADDRESS2.unwrap())
981            .city(CITY)
982            .state(STATE)
983            .zip(ZIP)
984            .build();
985
986        assert!(
987            build_result.as_ref().is_ok_and(|req| req.email.is_some()
988                && req.phone.is_none()
989                && req.address.is_some()),
990            "Expected Ok(VerificationRequest) w/ Some(email) & Some(address), got: {:#?}",
991            build_result.as_ref(),
992        );
993
994        // Phone & Address Request
995        build_result = super::VerificationRequest::builder()
996            .phone(PHONE)
997            .address1(ADDRESS1)
998            .address2(ADDRESS2.unwrap())
999            .city(CITY)
1000            .state(STATE)
1001            .zip(ZIP)
1002            .build();
1003
1004        assert!(
1005            build_result.as_ref().is_ok_and(|req| req.email.is_none()
1006                && req.phone.is_some()
1007                && req.address.is_some()),
1008            "Expected Ok(VerificationRequest) w/ Some(phone) & Some(address), got: {:#?}",
1009            build_result.as_ref(),
1010        );
1011
1012        // "Full" Request
1013        build_result = super::VerificationRequest::from_values(
1014            Some(EMAIL),
1015            Some(PHONE),
1016            Some(ADDRESS1),
1017            ADDRESS2,
1018            Some(CITY),
1019            Some(STATE),
1020            Some(ZIP),
1021        );
1022
1023        assert!(
1024            build_result.as_ref().is_ok_and(|req| req.email.is_some()
1025                && req.phone.is_some()
1026                && req.address.is_some()),
1027            "Expected Ok(VerificationRequest) w/ all fields populated, got: {:#?}",
1028            build_result.as_ref(),
1029        );
1030    }
1031}
1032
1033// </editor-fold desc="// I/O-Free Tests ...">