1extern crate alloc;
4
5use alloc::string::{String, ToString};
6
7use crate::coords::Coords;
8use crate::country::CountryCode;
9use crate::error::GeoError;
10use crate::postcode::{UkPostcode, UsZipCode};
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(tag = "type", content = "value"))]
16pub enum PostcodeChoice {
17 Uk(UkPostcode),
19 Us(UsZipCode),
21}
22
23impl core::fmt::Display for PostcodeChoice {
24 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
25 match self {
26 PostcodeChoice::Uk(x) => f.write_str(x.as_str()),
27 PostcodeChoice::Us(x) => f.write_str(x.as_str()),
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub struct Address {
44 pub line1: String,
46 pub line2: Option<String>,
48 pub city: String,
50 pub county: Option<String>,
52 pub postcode: UkPostcode,
54 pub country: CountryCode,
56 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
58 pub coords: Option<Coords>,
59}
60
61impl Address {
62 pub fn new(
71 line1: String,
72 line2: Option<String>,
73 city: String,
74 county: Option<String>,
75 postcode: UkPostcode,
76 country: CountryCode,
77 ) -> Result<Self, GeoError> {
78 Self::new_with_coords(line1, line2, city, county, postcode, country, None)
79 }
80
81 pub fn new_with_coords(
88 line1: String,
89 line2: Option<String>,
90 city: String,
91 county: Option<String>,
92 postcode: UkPostcode,
93 country: CountryCode,
94 coords: Option<Coords>,
95 ) -> Result<Self, GeoError> {
96 validate_address_fields(&line1, &line2, &city, &county)?;
97 Ok(Address {
98 line1: line1.trim().to_string(),
99 line2: line2.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()),
100 city: city.trim().to_string(),
101 county: county.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()),
102 postcode,
103 country,
104 coords,
105 })
106 }
107
108 #[must_use]
110 pub fn line1(&self) -> &str {
111 &self.line1
112 }
113
114 #[must_use]
116 pub fn city(&self) -> &str {
117 &self.city
118 }
119
120 #[must_use]
122 pub fn postcode(&self) -> &UkPostcode {
123 &self.postcode
124 }
125
126 #[must_use]
128 pub fn country(&self) -> &CountryCode {
129 &self.country
130 }
131}
132
133fn validate_address_fields(
134 line1: &str,
135 line2: &Option<String>,
136 city: &str,
137 county: &Option<String>,
138) -> Result<(), GeoError> {
139 if line1.trim().is_empty() {
140 return Err(GeoError::InvalidAddress(
141 "line1 must be non-empty".to_string(),
142 ));
143 }
144 if city.trim().is_empty() {
145 return Err(GeoError::InvalidAddress(
146 "city must be non-empty".to_string(),
147 ));
148 }
149 if line1.contains('\r') || line1.contains('\n') {
150 return Err(GeoError::InvalidAddress(
151 "line1 must not contain CR or LF".to_string(),
152 ));
153 }
154 if city.contains('\r') || city.contains('\n') {
155 return Err(GeoError::InvalidAddress(
156 "city must not contain CR or LF".to_string(),
157 ));
158 }
159 if let Some(l2) = line2 {
160 if l2.trim().is_empty() {
161 return Err(GeoError::InvalidAddress(
162 "line2 must be non-empty if provided".to_string(),
163 ));
164 }
165 if l2.contains('\r') || l2.contains('\n') {
166 return Err(GeoError::InvalidAddress(
167 "line2 must not contain CR or LF".to_string(),
168 ));
169 }
170 }
171 if let Some(c) = county {
172 if c.trim().is_empty() {
173 return Err(GeoError::InvalidAddress(
174 "county must be non-empty if provided".to_string(),
175 ));
176 }
177 if c.contains('\r') || c.contains('\n') {
178 return Err(GeoError::InvalidAddress(
179 "county must not contain CR or LF".to_string(),
180 ));
181 }
182 }
183 Ok(())
184}
185
186#[derive(Debug, Clone, Default)]
190pub struct AddressBuilder {
191 line1: Option<String>,
192 line2: Option<String>,
193 city: Option<String>,
194 county: Option<String>,
195 postcode: Option<UkPostcode>,
196 country: Option<CountryCode>,
197 coords: Option<Coords>,
198}
199
200impl AddressBuilder {
201 #[must_use]
203 pub fn new() -> Self {
204 Self::default()
205 }
206
207 #[must_use]
209 pub fn line1(mut self, s: impl Into<String>) -> Self {
210 self.line1 = Some(s.into());
211 self
212 }
213
214 #[must_use]
216 pub fn line2(mut self, s: impl Into<String>) -> Self {
217 self.line2 = Some(s.into());
218 self
219 }
220
221 #[must_use]
223 pub fn city(mut self, s: impl Into<String>) -> Self {
224 self.city = Some(s.into());
225 self
226 }
227
228 #[must_use]
230 pub fn county(mut self, s: impl Into<String>) -> Self {
231 self.county = Some(s.into());
232 self
233 }
234
235 #[must_use]
237 pub fn postcode(mut self, p: UkPostcode) -> Self {
238 self.postcode = Some(p);
239 self
240 }
241
242 #[must_use]
244 pub fn country(mut self, c: CountryCode) -> Self {
245 self.country = Some(c);
246 self
247 }
248
249 #[must_use]
251 pub fn coords(mut self, c: Coords) -> Self {
252 self.coords = Some(c);
253 self
254 }
255
256 pub fn build(self) -> Result<Address, GeoError> {
262 let line1 = self.line1.ok_or_else(|| {
263 GeoError::InvalidAddress("line1 is required".to_string())
264 })?;
265 let city = self.city.ok_or_else(|| {
266 GeoError::InvalidAddress("city is required".to_string())
267 })?;
268 let postcode = self.postcode.ok_or_else(|| {
269 GeoError::InvalidAddress("postcode is required".to_string())
270 })?;
271 let country = self.country.ok_or_else(|| {
272 GeoError::InvalidAddress("country is required".to_string())
273 })?;
274 Address::new_with_coords(line1, self.line2, city, self.county, postcode, country, self.coords)
275 }
276}
277
278impl core::fmt::Display for Address {
279 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280 write!(f, "{}, {}", self.line1, self.city)?;
281 if let Some(l2) = &self.line2 {
282 write!(f, " ({})", l2)?;
283 }
284 write!(f, ", {}", self.postcode)?;
285 write!(f, ", {}", self.country)?;
286 if let Some(coords) = &self.coords {
287 write!(f, " @ {}", coords)?;
288 }
289 Ok(())
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use crate::country::CountryCode;
297 use crate::postcode::UkPostcode;
298
299 fn uk(s: &str) -> UkPostcode {
300 UkPostcode::parse(s).expect("valid uk postcode")
301 }
302 fn cc(s: &str) -> CountryCode {
303 CountryCode::parse(s).expect("valid country")
304 }
305
306 #[test]
307 fn valid_address() {
308 let a = Address::new(
309 "10 Downing St".to_string(),
310 None,
311 "London".to_string(),
312 None,
313 uk("SW1A 2AA"),
314 cc("GB"),
315 )
316 .expect("valid");
317 assert_eq!(a.line1(), "10 Downing St");
318 assert_eq!(a.city(), "London");
319 }
320
321 #[test]
322 fn valid_with_all_fields() {
323 let a = AddressBuilder::new()
324 .line1("221B Baker St")
325 .line2("Flat B")
326 .city("London")
327 .county("Greater London")
328 .postcode(uk("NW1 6XE"))
329 .country(cc("GB"))
330 .build()
331 .expect("valid");
332 assert_eq!(a.line2.as_deref(), Some("Flat B"));
333 assert_eq!(a.county.as_deref(), Some("Greater London"));
334 }
335
336 #[test]
337 fn valid_with_coords() {
338 let coords = Coords::new(51.5, -0.12).expect("valid coords");
339 let a = Address::new_with_coords(
340 "10 Downing St".to_string(),
341 None,
342 "London".to_string(),
343 None,
344 uk("SW1A 2AA"),
345 cc("GB"),
346 Some(coords),
347 )
348 .expect("valid");
349 assert_eq!(a.coords, Some(coords));
350 }
351
352 #[test]
353 fn invalid_empty_line1() {
354 let res = Address::new(
355 " ".to_string(),
356 None,
357 "London".to_string(),
358 None,
359 uk("SW1A 2AA"),
360 cc("GB"),
361 );
362 assert!(res.is_err());
363 }
364
365 #[test]
366 fn invalid_empty_city() {
367 let res = Address::new(
368 "10 Downing St".to_string(),
369 None,
370 "".to_string(),
371 None,
372 uk("SW1A 2AA"),
373 cc("GB"),
374 );
375 assert!(res.is_err());
376 }
377
378 #[test]
379 fn invalid_line2_empty() {
380 let res = Address::new(
381 "10 Downing St".to_string(),
382 Some(" ".to_string()),
383 "London".to_string(),
384 None,
385 uk("SW1A 2AA"),
386 cc("GB"),
387 );
388 assert!(res.is_err());
389 }
390
391 #[test]
392 fn builder_missing_field() {
393 let res = AddressBuilder::new().line1("10 Downing St").city("London").build();
394 assert!(res.is_err());
395 }
396}