Skip to main content

use_address/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn trimmed_non_empty(value: impl AsRef<str>) -> Option<String> {
8    let trimmed = value.as_ref().trim();
9
10    (!trimmed.is_empty()).then(|| trimmed.to_string())
11}
12
13fn is_ascii_safe(value: &str) -> bool {
14    value
15        .chars()
16        .all(|character| character.is_ascii() && !character.is_ascii_control())
17}
18
19macro_rules! impl_trimmed_text_type {
20    ($type_name:ident, $error_name:ident, $empty_message:literal) => {
21        #[derive(Clone, Copy, Debug, Eq, PartialEq)]
22        pub enum $error_name {
23            Empty,
24        }
25
26        impl fmt::Display for $error_name {
27            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28                match self {
29                    Self::Empty => formatter.write_str($empty_message),
30                }
31            }
32        }
33
34        impl Error for $error_name {}
35
36        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37        pub struct $type_name(String);
38
39        impl $type_name {
40            /// Creates a value from non-empty text.
41            ///
42            /// # Errors
43            ///
44            /// Returns [`Self::Error::Empty`] when the trimmed value is empty.
45            pub fn new(value: impl AsRef<str>) -> Result<Self, $error_name> {
46                trimmed_non_empty(value).ok_or($error_name::Empty).map(Self)
47            }
48
49            #[must_use]
50            pub fn as_str(&self) -> &str {
51                &self.0
52            }
53        }
54
55        impl AsRef<str> for $type_name {
56            fn as_ref(&self) -> &str {
57                self.as_str()
58            }
59        }
60
61        impl fmt::Display for $type_name {
62            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63                formatter.write_str(self.as_str())
64            }
65        }
66
67        impl FromStr for $type_name {
68            type Err = $error_name;
69
70            fn from_str(value: &str) -> Result<Self, Self::Err> {
71                Self::new(value)
72            }
73        }
74
75        impl TryFrom<&str> for $type_name {
76            type Error = $error_name;
77
78            fn try_from(value: &str) -> Result<Self, Self::Error> {
79                Self::new(value)
80            }
81        }
82    };
83}
84
85impl_trimmed_text_type!(
86    AddressLine,
87    AddressLineError,
88    "address line cannot be empty"
89);
90impl_trimmed_text_type!(
91    AddressLine1,
92    AddressLine1Error,
93    "address line 1 cannot be empty"
94);
95impl_trimmed_text_type!(
96    AddressLine2,
97    AddressLine2Error,
98    "address line 2 cannot be empty"
99);
100impl_trimmed_text_type!(
101    StreetNumber,
102    StreetNumberError,
103    "street number cannot be empty"
104);
105impl_trimmed_text_type!(StreetName, StreetNameError, "street name cannot be empty");
106impl_trimmed_text_type!(
107    UnitDesignator,
108    UnitDesignatorError,
109    "unit designator cannot be empty"
110);
111impl_trimmed_text_type!(UnitNumber, UnitNumberError, "unit number cannot be empty");
112impl_trimmed_text_type!(Locality, LocalityError, "locality cannot be empty");
113impl_trimmed_text_type!(
114    AdministrativeArea,
115    AdministrativeAreaError,
116    "administrative area cannot be empty"
117);
118impl_trimmed_text_type!(
119    CountrySubdivision,
120    CountrySubdivisionError,
121    "country subdivision cannot be empty"
122);
123
124impl From<AddressLine1> for AddressLine {
125    fn from(value: AddressLine1) -> Self {
126        Self(value.0)
127    }
128}
129
130impl From<AddressLine2> for AddressLine {
131    fn from(value: AddressLine2) -> Self {
132        Self(value.0)
133    }
134}
135
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub enum PostalCodeError {
138    Empty,
139    InvalidCharacter,
140}
141
142impl fmt::Display for PostalCodeError {
143    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144        match self {
145            Self::Empty => formatter.write_str("postal code cannot be empty"),
146            Self::InvalidCharacter => {
147                formatter.write_str("postal code must contain only ASCII-safe characters")
148            },
149        }
150    }
151}
152
153impl Error for PostalCodeError {}
154
155#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
156pub struct PostalCode(String);
157
158impl PostalCode {
159    /// Creates a postal code from non-empty ASCII-safe text.
160    ///
161    /// # Errors
162    ///
163    /// Returns [`PostalCodeError::Empty`] when the trimmed value is empty.
164    /// Returns [`PostalCodeError::InvalidCharacter`] when the value contains
165    /// non-ASCII or control characters.
166    pub fn new(value: impl AsRef<str>) -> Result<Self, PostalCodeError> {
167        let trimmed = value.as_ref().trim();
168
169        if trimmed.is_empty() {
170            return Err(PostalCodeError::Empty);
171        }
172
173        if !is_ascii_safe(trimmed) {
174            return Err(PostalCodeError::InvalidCharacter);
175        }
176
177        Ok(Self(trimmed.to_string()))
178    }
179
180    #[must_use]
181    pub fn as_str(&self) -> &str {
182        &self.0
183    }
184}
185
186impl AsRef<str> for PostalCode {
187    fn as_ref(&self) -> &str {
188        self.as_str()
189    }
190}
191
192impl fmt::Display for PostalCode {
193    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
194        formatter.write_str(self.as_str())
195    }
196}
197
198impl FromStr for PostalCode {
199    type Err = PostalCodeError;
200
201    fn from_str(value: &str) -> Result<Self, Self::Err> {
202        Self::new(value)
203    }
204}
205
206impl TryFrom<&str> for PostalCode {
207    type Error = PostalCodeError;
208
209    fn try_from(value: &str) -> Result<Self, Self::Error> {
210        Self::new(value)
211    }
212}
213
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub enum AddressCountryCodeError {
216    Empty,
217    InvalidLength,
218    InvalidCharacter,
219}
220
221impl fmt::Display for AddressCountryCodeError {
222    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match self {
224            Self::Empty => formatter.write_str("address country code cannot be empty"),
225            Self::InvalidLength => formatter
226                .write_str("address country code must contain exactly two alphabetic characters"),
227            Self::InvalidCharacter => formatter
228                .write_str("address country code must contain only ASCII alphabetic characters"),
229        }
230    }
231}
232
233impl Error for AddressCountryCodeError {}
234
235#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
236pub struct AddressCountryCode(String);
237
238impl AddressCountryCode {
239    /// Creates an uppercase alpha-2 address country code.
240    ///
241    /// # Errors
242    ///
243    /// Returns [`AddressCountryCodeError::Empty`] when the trimmed value is
244    /// empty.
245    /// Returns [`AddressCountryCodeError::InvalidLength`] when the value does
246    /// not contain exactly two characters.
247    /// Returns [`AddressCountryCodeError::InvalidCharacter`] when the value
248    /// contains non-ASCII alphabetic characters.
249    pub fn new(value: impl AsRef<str>) -> Result<Self, AddressCountryCodeError> {
250        let trimmed = value.as_ref().trim();
251        let character_count = trimmed.chars().count();
252
253        if trimmed.is_empty() {
254            return Err(AddressCountryCodeError::Empty);
255        }
256
257        if character_count != 2 {
258            return Err(AddressCountryCodeError::InvalidLength);
259        }
260
261        if !trimmed
262            .chars()
263            .all(|character| character.is_ascii_alphabetic())
264        {
265            return Err(AddressCountryCodeError::InvalidCharacter);
266        }
267
268        Ok(Self(trimmed.to_ascii_uppercase()))
269    }
270
271    #[must_use]
272    pub fn as_str(&self) -> &str {
273        &self.0
274    }
275}
276
277impl AsRef<str> for AddressCountryCode {
278    fn as_ref(&self) -> &str {
279        self.as_str()
280    }
281}
282
283impl fmt::Display for AddressCountryCode {
284    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
285        formatter.write_str(self.as_str())
286    }
287}
288
289impl FromStr for AddressCountryCode {
290    type Err = AddressCountryCodeError;
291
292    fn from_str(value: &str) -> Result<Self, Self::Err> {
293        Self::new(value)
294    }
295}
296
297impl TryFrom<&str> for AddressCountryCode {
298    type Error = AddressCountryCodeError;
299
300    fn try_from(value: &str) -> Result<Self, Self::Error> {
301        Self::new(value)
302    }
303}
304
305#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
306pub enum AddressComponentKind {
307    Line1,
308    Line2,
309    StreetNumber,
310    StreetName,
311    UnitDesignator,
312    UnitNumber,
313    Locality,
314    AdministrativeArea,
315    PostalCode,
316    Country,
317    CountrySubdivision,
318    Region,
319    Landmark,
320    CareOf,
321    Attention,
322    Organization,
323    Other,
324}
325
326#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
327pub enum AddressFormatHint {
328    #[default]
329    Unknown,
330    UnitedStatesLike,
331    CanadaLike,
332    UnitedKingdomLike,
333    EuropeanLike,
334    JapanLike,
335    International,
336    Custom,
337}
338
339#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
340pub enum AddressValidationLevel {
341    #[default]
342    Unvalidated,
343    SyntaxChecked,
344    ComponentChecked,
345    PostalChecked,
346    Geocoded,
347    Verified,
348}
349
350#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
351pub enum AddressKind {
352    #[default]
353    Physical,
354    Mailing,
355    Billing,
356    Shipping,
357    Legal,
358    Registered,
359    Service,
360    Other,
361}
362
363#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
364pub enum AddressUsageKind {
365    Residential,
366    Commercial,
367    Industrial,
368    Government,
369    Educational,
370    Healthcare,
371    Mixed,
372    #[default]
373    Unknown,
374}
375
376#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
377pub enum AddressPrecision {
378    #[default]
379    Unknown,
380    Country,
381    Region,
382    Locality,
383    PostalCode,
384    Street,
385    Building,
386    Unit,
387}
388
389#[derive(Clone, Debug, Eq, Hash, PartialEq)]
390pub struct StreetAddress {
391    pub number: StreetNumber,
392    pub name: StreetName,
393    pub unit_designator: Option<UnitDesignator>,
394    pub unit_number: Option<UnitNumber>,
395}
396
397impl StreetAddress {
398    #[must_use]
399    pub const fn new(number: StreetNumber, name: StreetName) -> Self {
400        Self {
401            number,
402            name,
403            unit_designator: None,
404            unit_number: None,
405        }
406    }
407
408    #[must_use]
409    pub fn with_unit(mut self, designator: UnitDesignator, number: UnitNumber) -> Self {
410        self.unit_designator = Some(designator);
411        self.unit_number = Some(number);
412        self
413    }
414}
415
416#[derive(Clone, Debug, Eq, PartialEq)]
417pub enum AddressError {
418    Line(AddressLineError),
419    PostalCode(PostalCodeError),
420    Locality(LocalityError),
421    AdministrativeArea(AdministrativeAreaError),
422    CountryCode(AddressCountryCodeError),
423}
424
425impl fmt::Display for AddressError {
426    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
427        match self {
428            Self::Line(error) => error.fmt(formatter),
429            Self::PostalCode(error) => error.fmt(formatter),
430            Self::Locality(error) => error.fmt(formatter),
431            Self::AdministrativeArea(error) => error.fmt(formatter),
432            Self::CountryCode(error) => error.fmt(formatter),
433        }
434    }
435}
436
437impl Error for AddressError {
438    fn source(&self) -> Option<&(dyn Error + 'static)> {
439        match self {
440            Self::Line(error) => Some(error),
441            Self::PostalCode(error) => Some(error),
442            Self::Locality(error) => Some(error),
443            Self::AdministrativeArea(error) => Some(error),
444            Self::CountryCode(error) => Some(error),
445        }
446    }
447}
448
449impl From<AddressLineError> for AddressError {
450    fn from(value: AddressLineError) -> Self {
451        Self::Line(value)
452    }
453}
454
455impl From<PostalCodeError> for AddressError {
456    fn from(value: PostalCodeError) -> Self {
457        Self::PostalCode(value)
458    }
459}
460
461impl From<LocalityError> for AddressError {
462    fn from(value: LocalityError) -> Self {
463        Self::Locality(value)
464    }
465}
466
467impl From<AdministrativeAreaError> for AddressError {
468    fn from(value: AdministrativeAreaError) -> Self {
469        Self::AdministrativeArea(value)
470    }
471}
472
473impl From<AddressCountryCodeError> for AddressError {
474    fn from(value: AddressCountryCodeError) -> Self {
475        Self::CountryCode(value)
476    }
477}
478
479#[derive(Clone, Debug, Eq, PartialEq)]
480pub struct Address {
481    pub lines: Vec<AddressLine>,
482    pub locality: Option<Locality>,
483    pub administrative_area: Option<AdministrativeArea>,
484    pub postal_code: Option<PostalCode>,
485    pub country_code: Option<AddressCountryCode>,
486    pub kind: AddressKind,
487    pub usage: AddressUsageKind,
488    pub format_hint: AddressFormatHint,
489    pub validation_level: AddressValidationLevel,
490    pub precision: AddressPrecision,
491}
492
493impl Address {
494    #[must_use]
495    pub const fn new() -> Self {
496        Self {
497            lines: Vec::new(),
498            locality: None,
499            administrative_area: None,
500            postal_code: None,
501            country_code: None,
502            kind: AddressKind::Physical,
503            usage: AddressUsageKind::Unknown,
504            format_hint: AddressFormatHint::Unknown,
505            validation_level: AddressValidationLevel::Unvalidated,
506            precision: AddressPrecision::Unknown,
507        }
508    }
509
510    #[must_use]
511    pub fn with_line(mut self, line: AddressLine) -> Self {
512        self.lines.push(line);
513        self
514    }
515
516    #[must_use]
517    pub const fn is_empty(&self) -> bool {
518        self.lines.is_empty()
519            && self.locality.is_none()
520            && self.administrative_area.is_none()
521            && self.postal_code.is_none()
522            && self.country_code.is_none()
523    }
524
525    #[must_use]
526    pub const fn has_country(&self) -> bool {
527        self.country_code.is_some()
528    }
529
530    #[must_use]
531    pub const fn has_postal_code(&self) -> bool {
532        self.postal_code.is_some()
533    }
534
535    #[must_use]
536    pub const fn has_locality(&self) -> bool {
537        self.locality.is_some()
538    }
539
540    #[must_use]
541    pub fn component_count(&self) -> usize {
542        self.lines.len()
543            + usize::from(self.locality.is_some())
544            + usize::from(self.administrative_area.is_some())
545            + usize::from(self.postal_code.is_some())
546            + usize::from(self.country_code.is_some())
547    }
548}
549
550impl Default for Address {
551    fn default() -> Self {
552        Self::new()
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::{
559        Address, AddressCountryCode, AddressCountryCodeError, AddressError, AddressLine,
560        AddressLine1, AddressLine1Error, AddressLine2, AddressLine2Error, AddressLineError,
561        AdministrativeArea, AdministrativeAreaError, CountrySubdivision, CountrySubdivisionError,
562        Locality, LocalityError, PostalCode, PostalCodeError, StreetAddress, StreetName,
563        StreetNameError, StreetNumber, StreetNumberError, UnitDesignator, UnitDesignatorError,
564        UnitNumber, UnitNumberError,
565    };
566
567    macro_rules! non_empty_text_tests {
568        ($valid_name:ident, $invalid_name:ident, $type_name:ty, $error_name:ty, $input:literal, $expected:literal) => {
569            #[test]
570            fn $valid_name() -> Result<(), $error_name> {
571                let value = <$type_name>::new($input)?;
572
573                assert_eq!(value.as_str(), $expected);
574                Ok(())
575            }
576
577            #[test]
578            fn $invalid_name() {
579                assert_eq!(<$type_name>::new("   "), Err(<$error_name>::Empty));
580            }
581        };
582    }
583
584    non_empty_text_tests!(
585        address_line_accepts_trimmed_text,
586        address_line_rejects_empty_text,
587        AddressLine,
588        AddressLineError,
589        " 123 Main St ",
590        "123 Main St"
591    );
592    non_empty_text_tests!(
593        address_line1_accepts_trimmed_text,
594        address_line1_rejects_empty_text,
595        AddressLine1,
596        AddressLine1Error,
597        " 456 Oak Ave ",
598        "456 Oak Ave"
599    );
600    non_empty_text_tests!(
601        address_line2_accepts_trimmed_text,
602        address_line2_rejects_empty_text,
603        AddressLine2,
604        AddressLine2Error,
605        " Suite 9 ",
606        "Suite 9"
607    );
608    non_empty_text_tests!(
609        street_number_accepts_trimmed_text,
610        street_number_rejects_empty_text,
611        StreetNumber,
612        StreetNumberError,
613        " 221B ",
614        "221B"
615    );
616    non_empty_text_tests!(
617        street_name_accepts_trimmed_text,
618        street_name_rejects_empty_text,
619        StreetName,
620        StreetNameError,
621        " Baker Street ",
622        "Baker Street"
623    );
624    non_empty_text_tests!(
625        unit_designator_accepts_trimmed_text,
626        unit_designator_rejects_empty_text,
627        UnitDesignator,
628        UnitDesignatorError,
629        " Apt ",
630        "Apt"
631    );
632    non_empty_text_tests!(
633        unit_number_accepts_trimmed_text,
634        unit_number_rejects_empty_text,
635        UnitNumber,
636        UnitNumberError,
637        " 4B ",
638        "4B"
639    );
640    non_empty_text_tests!(
641        locality_accepts_trimmed_text,
642        locality_rejects_empty_text,
643        Locality,
644        LocalityError,
645        " London ",
646        "London"
647    );
648    non_empty_text_tests!(
649        administrative_area_accepts_trimmed_text,
650        administrative_area_rejects_empty_text,
651        AdministrativeArea,
652        AdministrativeAreaError,
653        " Indiana ",
654        "Indiana"
655    );
656    non_empty_text_tests!(
657        country_subdivision_accepts_trimmed_text,
658        country_subdivision_rejects_empty_text,
659        CountrySubdivision,
660        CountrySubdivisionError,
661        " Ontario ",
662        "Ontario"
663    );
664
665    #[test]
666    fn postal_code_accepts_numeric_zip() -> Result<(), PostalCodeError> {
667        let postal_code = PostalCode::new("46802")?;
668
669        assert_eq!(postal_code.as_str(), "46802");
670        Ok(())
671    }
672
673    #[test]
674    fn postal_code_accepts_alphanumeric_code() -> Result<(), PostalCodeError> {
675        let postal_code = PostalCode::new("SW1A 1AA")?;
676
677        assert_eq!(postal_code.as_str(), "SW1A 1AA");
678        Ok(())
679    }
680
681    #[test]
682    fn postal_code_rejects_empty_text() {
683        assert_eq!(PostalCode::new("   "), Err(PostalCodeError::Empty));
684    }
685
686    #[test]
687    fn postal_code_rejects_non_ascii_text() {
688        assert_eq!(
689            PostalCode::new("〒100-0001"),
690            Err(PostalCodeError::InvalidCharacter)
691        );
692    }
693
694    #[test]
695    fn country_code_accepts_uppercase_alpha2() -> Result<(), AddressCountryCodeError> {
696        let country_code = AddressCountryCode::new("US")?;
697
698        assert_eq!(country_code.as_str(), "US");
699        Ok(())
700    }
701
702    #[test]
703    fn country_code_normalizes_lowercase_input() -> Result<(), AddressCountryCodeError> {
704        let country_code = AddressCountryCode::new("us")?;
705
706        assert_eq!(country_code.as_str(), "US");
707        Ok(())
708    }
709
710    #[test]
711    fn country_code_rejects_invalid_length() {
712        assert_eq!(
713            AddressCountryCode::new("USA"),
714            Err(AddressCountryCodeError::InvalidLength)
715        );
716    }
717
718    #[test]
719    fn country_code_rejects_invalid_characters() {
720        assert_eq!(
721            AddressCountryCode::new("1A"),
722            Err(AddressCountryCodeError::InvalidCharacter)
723        );
724    }
725
726    #[test]
727    fn country_code_rejects_empty_text() {
728        assert_eq!(
729            AddressCountryCode::new("   "),
730            Err(AddressCountryCodeError::Empty)
731        );
732    }
733
734    #[test]
735    fn display_and_from_str_round_trip_for_address_line() -> Result<(), AddressLineError> {
736        let line = "123 Main St".parse::<AddressLine>()?;
737
738        assert_eq!(line.to_string(), "123 Main St");
739        Ok(())
740    }
741
742    #[test]
743    fn display_and_from_str_round_trip_for_country_code() -> Result<(), AddressCountryCodeError> {
744        let country_code = "us".parse::<AddressCountryCode>()?;
745
746        assert_eq!(country_code.to_string(), "US");
747        Ok(())
748    }
749
750    #[test]
751    fn try_from_supports_validated_newtypes() -> Result<(), AddressCountryCodeError> {
752        let country_code = AddressCountryCode::try_from("ca")?;
753
754        assert_eq!(country_code.as_str(), "CA");
755        Ok(())
756    }
757
758    #[test]
759    fn address_line_variants_convert_to_address_line() -> Result<(), AddressLine1Error> {
760        let line = AddressLine::from(AddressLine1::new("Apartment Lobby")?);
761
762        assert_eq!(line.as_str(), "Apartment Lobby");
763        Ok(())
764    }
765
766    #[test]
767    fn address_line2_converts_to_address_line() -> Result<(), AddressLine2Error> {
768        let line = AddressLine::from(AddressLine2::new("Floor 3")?);
769
770        assert_eq!(line.as_str(), "Floor 3");
771        Ok(())
772    }
773
774    #[test]
775    fn street_address_builder_sets_unit_fields() -> Result<(), Box<dyn std::error::Error>> {
776        let street_address =
777            StreetAddress::new(StreetNumber::new("221B")?, StreetName::new("Baker Street")?)
778                .with_unit(UnitDesignator::new("Apt")?, UnitNumber::new("4B")?);
779
780        assert_eq!(street_address.number.as_str(), "221B");
781        assert_eq!(street_address.name.as_str(), "Baker Street");
782        assert_eq!(
783            street_address
784                .unit_designator
785                .as_ref()
786                .map(UnitDesignator::as_str),
787            Some("Apt")
788        );
789        assert_eq!(
790            street_address.unit_number.as_ref().map(UnitNumber::as_str),
791            Some("4B")
792        );
793        Ok(())
794    }
795
796    #[test]
797    fn empty_address_is_empty() {
798        assert!(Address::new().is_empty());
799    }
800
801    #[test]
802    fn address_with_one_line_is_not_empty() -> Result<(), AddressLineError> {
803        let address = Address::new().with_line(AddressLine::new("123 Main St")?);
804
805        assert!(!address.is_empty());
806        Ok(())
807    }
808
809    #[test]
810    fn address_helpers_report_present_components() -> Result<(), Box<dyn std::error::Error>> {
811        let address = Address {
812            locality: Some(Locality::new("Fort Wayne")?),
813            postal_code: Some(PostalCode::new("46802")?),
814            country_code: Some(AddressCountryCode::new("us")?),
815            ..Address::new().with_line(AddressLine::new("123 Main St")?)
816        };
817
818        assert!(address.has_locality());
819        assert!(address.has_postal_code());
820        assert!(address.has_country());
821        Ok(())
822    }
823
824    #[test]
825    fn address_component_count_tracks_structural_components()
826    -> Result<(), Box<dyn std::error::Error>> {
827        let address = Address {
828            locality: Some(Locality::new("Fort Wayne")?),
829            administrative_area: Some(AdministrativeArea::new("Indiana")?),
830            postal_code: Some(PostalCode::new("46802")?),
831            country_code: Some(AddressCountryCode::new("us")?),
832            ..Address::new().with_line(AddressLine::new("123 Main St")?)
833        };
834
835        assert_eq!(address.component_count(), 5);
836        Ok(())
837    }
838
839    #[test]
840    fn address_error_wraps_component_errors() {
841        let error = AddressError::from(AddressCountryCodeError::InvalidCharacter);
842
843        assert_eq!(
844            error.to_string(),
845            "address country code must contain only ASCII alphabetic characters"
846        );
847    }
848}