postal_code/
postal_code.rs

1crate::ix!();
2
3/// A validated postal code with country context.
4#[derive(Builder,Debug,Hash,Clone,PartialEq,Eq,Serialize,Deserialize,Getters,Ord,PartialOrd)]
5#[builder(build_fn(error = "PostalCodeConstructionError",validate = "Self::validate"))]
6pub struct PostalCode {
7
8    /// The country associated with this postal code.
9    #[getset(get = "pub")]
10    country: Country,
11
12    /// The validated postal code string.
13    #[getset(get = "pub")]
14    code: String,
15}
16
17impl PostalCode {
18
19    pub fn new(country: Country, code: &str) -> Result<Self,PostalCodeConstructionError> {
20        PostalCodeBuilder::default()
21            .country(country)
22            .code(code.to_string())
23            .build()
24    }
25}
26
27impl std::ops::Deref for PostalCode {
28
29    type Target = String;
30
31    fn deref(&self) -> &Self::Target {
32        &self.code
33    }
34}
35
36impl PostalCodeBuilder {
37    pub fn validate(&self) -> Result<(), PostalCodeConstructionError> {
38        if self.country.is_none() || self.code.is_none() {
39            return Err(PostalCodeConstructionError::InvalidFormat {
40                attempted_code: "<unset>".to_string(),
41                attempted_country: None,
42            });
43        }
44        let country = self.country.as_ref().unwrap();
45        let code = self.code.as_ref().unwrap();
46
47        if let Some(validator) = country.get_postal_code_validator() {
48            if validator.validate(code) {
49                Ok(())
50            } else {
51                Err(PostalCodeConstructionError::InvalidFormat {
52                    attempted_code: code.clone(),
53                    attempted_country: Some(country.clone()),
54                })
55            }
56        } else {
57            Err(PostalCodeConstructionError::UnsupportedCountry {
58                attempted_country: country.clone(),
59            })
60        }
61    }
62}
63
64#[cfg(test)]
65mod postal_code_tests {
66    use super::*;
67    use country::Country;
68    use rand::Rng; // If you add rand to dev-dependencies for randomness tests
69
70    #[test]
71    fn test_us_valid() {
72        let pc = PostalCode::new(Country::USA, "12345");
73        assert!(pc.is_ok());
74        assert_eq!(pc.unwrap().code(), "12345");
75    }
76
77    #[test]
78    fn test_us_valid_zip_plus4() {
79        let pc = PostalCode::new(Country::USA, "12345-6789");
80        assert!(pc.is_ok());
81    }
82
83    #[test]
84    fn test_us_invalid_alphabetic() {
85        let pc = PostalCode::new(Country::USA, "ABCDE");
86        assert!(pc.is_err());
87        if let Err(PostalCodeConstructionError::InvalidFormat { attempted_code, attempted_country }) = pc {
88            assert_eq!(attempted_code, "ABCDE");
89            assert_eq!(attempted_country, Some(Country::USA));
90        } else {
91            panic!("Unexpected error type");
92        }
93    }
94
95    #[test]
96    fn test_us_invalid_length() {
97        let pc = PostalCode::new(Country::USA, "1234");
98        assert!(pc.is_err());
99    }
100
101    #[test]
102    fn test_ca_valid() {
103        let pc = PostalCode::new(Country::Canada, "K1A0B1");
104        assert!(pc.is_ok());
105    }
106
107    #[test]
108    fn test_ca_valid_with_space() {
109        // Common Canadian formatting includes a space after the first three characters.
110        let pc = PostalCode::new(Country::Canada, "K1A 0B1");
111        assert!(pc.is_ok());
112    }
113
114    #[test]
115    fn test_ca_invalid() {
116        let pc = PostalCode::new(Country::Canada, "123456");
117        assert!(pc.is_err());
118    }
119
120    #[test]
121    fn test_ca_invalid_non_alphanumeric() {
122        // Contains a symbol that's not allowed
123        let pc = PostalCode::new(Country::Canada, "K1A!0B1");
124        assert!(pc.is_err());
125    }
126
127    #[test]
128    fn test_uk_valid() {
129        // Buckingham Palace code: "SW1A 1AA"
130        let pc = PostalCode::new(Country::UnitedKingdom, "SW1A 1AA");
131        assert!(pc.is_ok());
132    }
133
134    #[test]
135    fn test_uk_invalid_no_space() {
136        // UK codes typically have a space; this might fail validation
137        let pc = PostalCode::new(Country::UnitedKingdom, "SW1A1AA");
138        assert!(pc.is_err());
139    }
140
141    #[test]
142    fn test_uk_invalid_too_long() {
143        let pc = PostalCode::new(Country::UnitedKingdom, "SW1A 1AAA");
144        assert!(pc.is_err());
145    }
146
147    #[test]
148    fn test_fr_valid() {
149        let pc = PostalCode::new(Country::France, "75001");
150        assert!(pc.is_ok());
151    }
152
153    #[test]
154    fn test_fr_invalid_short() {
155        let pc = PostalCode::new(Country::France, "7500");
156        assert!(pc.is_err());
157    }
158
159    #[test]
160    fn test_fr_invalid_alpha() {
161        let pc = PostalCode::new(Country::France, "75A01");
162        assert!(pc.is_err());
163    }
164
165    #[test]
166    fn test_de_valid() {
167        let pc = PostalCode::new(Country::Germany, "10115");
168        assert!(pc.is_ok());
169    }
170
171    #[test]
172    fn test_de_invalid_short() {
173        let pc = PostalCode::new(Country::Germany, "101");
174        assert!(pc.is_err());
175    }
176
177    #[test]
178    fn test_de_invalid_alpha() {
179        let pc = PostalCode::new(Country::Germany, "10A15");
180        assert!(pc.is_err());
181    }
182
183    #[test]
184    fn test_it_valid() {
185        let pc = PostalCode::new(Country::Italy, "00144");
186        assert!(pc.is_ok());
187    }
188
189    #[test]
190    fn test_it_invalid_short() {
191        let pc = PostalCode::new(Country::Italy, "0144");
192        assert!(pc.is_err());
193    }
194
195    #[test]
196    fn test_it_invalid_alpha() {
197        let pc = PostalCode::new(Country::Italy, "00A44");
198        assert!(pc.is_err());
199    }
200
201    #[test]
202    fn test_unsupported_country() {
203        let pc = PostalCode::new(Country::Uzbekistan, "12345");
204        assert!(pc.is_err());
205        if let Err(PostalCodeConstructionError::UnsupportedCountry { attempted_country }) = pc {
206            assert_eq!(attempted_country, Country::Uzbekistan);
207        } else {
208            panic!("Expected UnsupportedCountry error");
209        }
210    }
211
212    #[test]
213    fn test_missing_fields_via_builder() {
214        // If we try to build without setting required fields, we should get a suitable error.
215        let pc = PostalCodeBuilder::default().build(); 
216        assert!(pc.is_err());
217        if let Err(PostalCodeConstructionError::InvalidFormat { attempted_code, attempted_country }) = pc {
218            assert_eq!(attempted_code, "<unset>");
219            assert_eq!(attempted_country, None);
220        } else {
221            panic!("Expected InvalidFormat error due to missing fields");
222        }
223    }
224
225    #[test]
226    fn test_multiple_random_us_codes() {
227        // Test random US ZIP codes
228        // Just ensure they pass the regex if they follow correct format
229        let mut rng = rand::thread_rng();
230        for _ in 0..10 {
231            let base: u32 = rng.gen_range(0..100000);
232            let code = format!("{:05}", base);
233            let pc = PostalCode::new(Country::USA, &code);
234            assert!(pc.is_ok());
235        }
236        // Test some random invalid US codes with letters
237        for _ in 0..5 {
238            let code = format!("{}ABCD", rng.gen_range(0..10000));
239            let pc = PostalCode::new(Country::USA, &code);
240            assert!(pc.is_err());
241        }
242    }
243
244    #[test]
245    fn test_space_and_hyphen_tolerance_in_ca_codes() {
246        // Canadian codes can have an optional space. Check that a hyphen fails.
247        let pc = PostalCode::new(Country::Canada, "K1A-0B1");
248        assert!(pc.is_err());
249    }
250
251    #[test]
252    fn test_uk_gir_special_case() {
253        // G.IR 0AA is a special National Girobank code. Check that it is valid.
254        let pc = PostalCode::new(Country::UnitedKingdom, "GIR 0AA");
255        assert!(pc.is_ok());
256    }
257}