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