extern crate alloc;
use alloc::string::{String, ToString};
use crate::coords::Coords;
use crate::country::CountryCode;
use crate::error::GeoError;
use crate::postcode::{UkPostcode, UsZipCode};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", content = "value"))]
pub enum PostcodeChoice {
Uk(UkPostcode),
Us(UsZipCode),
}
impl core::fmt::Display for PostcodeChoice {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
PostcodeChoice::Uk(x) => f.write_str(x.as_str()),
PostcodeChoice::Us(x) => f.write_str(x.as_str()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Address {
pub line1: String,
pub line2: Option<String>,
pub city: String,
pub county: Option<String>,
pub postcode: UkPostcode,
pub country: CountryCode,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub coords: Option<Coords>,
}
impl Address {
pub fn new(
line1: String,
line2: Option<String>,
city: String,
county: Option<String>,
postcode: UkPostcode,
country: CountryCode,
) -> Result<Self, GeoError> {
Self::new_with_coords(line1, line2, city, county, postcode, country, None)
}
pub fn new_with_coords(
line1: String,
line2: Option<String>,
city: String,
county: Option<String>,
postcode: UkPostcode,
country: CountryCode,
coords: Option<Coords>,
) -> Result<Self, GeoError> {
validate_address_fields(&line1, &line2, &city, &county)?;
Ok(Address {
line1: line1.trim().to_string(),
line2: line2.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()),
city: city.trim().to_string(),
county: county.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()),
postcode,
country,
coords,
})
}
#[must_use]
pub fn line1(&self) -> &str {
&self.line1
}
#[must_use]
pub fn city(&self) -> &str {
&self.city
}
#[must_use]
pub fn postcode(&self) -> &UkPostcode {
&self.postcode
}
#[must_use]
pub fn country(&self) -> &CountryCode {
&self.country
}
}
fn validate_address_fields(
line1: &str,
line2: &Option<String>,
city: &str,
county: &Option<String>,
) -> Result<(), GeoError> {
if line1.trim().is_empty() {
return Err(GeoError::InvalidAddress(
"line1 must be non-empty".to_string(),
));
}
if city.trim().is_empty() {
return Err(GeoError::InvalidAddress(
"city must be non-empty".to_string(),
));
}
if line1.contains('\r') || line1.contains('\n') {
return Err(GeoError::InvalidAddress(
"line1 must not contain CR or LF".to_string(),
));
}
if city.contains('\r') || city.contains('\n') {
return Err(GeoError::InvalidAddress(
"city must not contain CR or LF".to_string(),
));
}
if let Some(l2) = line2 {
if l2.trim().is_empty() {
return Err(GeoError::InvalidAddress(
"line2 must be non-empty if provided".to_string(),
));
}
if l2.contains('\r') || l2.contains('\n') {
return Err(GeoError::InvalidAddress(
"line2 must not contain CR or LF".to_string(),
));
}
}
if let Some(c) = county {
if c.trim().is_empty() {
return Err(GeoError::InvalidAddress(
"county must be non-empty if provided".to_string(),
));
}
if c.contains('\r') || c.contains('\n') {
return Err(GeoError::InvalidAddress(
"county must not contain CR or LF".to_string(),
));
}
}
Ok(())
}
#[derive(Debug, Clone, Default)]
pub struct AddressBuilder {
line1: Option<String>,
line2: Option<String>,
city: Option<String>,
county: Option<String>,
postcode: Option<UkPostcode>,
country: Option<CountryCode>,
coords: Option<Coords>,
}
impl AddressBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn line1(mut self, s: impl Into<String>) -> Self {
self.line1 = Some(s.into());
self
}
#[must_use]
pub fn line2(mut self, s: impl Into<String>) -> Self {
self.line2 = Some(s.into());
self
}
#[must_use]
pub fn city(mut self, s: impl Into<String>) -> Self {
self.city = Some(s.into());
self
}
#[must_use]
pub fn county(mut self, s: impl Into<String>) -> Self {
self.county = Some(s.into());
self
}
#[must_use]
pub fn postcode(mut self, p: UkPostcode) -> Self {
self.postcode = Some(p);
self
}
#[must_use]
pub fn country(mut self, c: CountryCode) -> Self {
self.country = Some(c);
self
}
#[must_use]
pub fn coords(mut self, c: Coords) -> Self {
self.coords = Some(c);
self
}
pub fn build(self) -> Result<Address, GeoError> {
let line1 = self.line1.ok_or_else(|| {
GeoError::InvalidAddress("line1 is required".to_string())
})?;
let city = self.city.ok_or_else(|| {
GeoError::InvalidAddress("city is required".to_string())
})?;
let postcode = self.postcode.ok_or_else(|| {
GeoError::InvalidAddress("postcode is required".to_string())
})?;
let country = self.country.ok_or_else(|| {
GeoError::InvalidAddress("country is required".to_string())
})?;
Address::new_with_coords(line1, self.line2, city, self.county, postcode, country, self.coords)
}
}
impl core::fmt::Display for Address {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}, {}", self.line1, self.city)?;
if let Some(l2) = &self.line2 {
write!(f, " ({})", l2)?;
}
write!(f, ", {}", self.postcode)?;
write!(f, ", {}", self.country)?;
if let Some(coords) = &self.coords {
write!(f, " @ {}", coords)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::country::CountryCode;
use crate::postcode::UkPostcode;
fn uk(s: &str) -> UkPostcode {
UkPostcode::parse(s).expect("valid uk postcode")
}
fn cc(s: &str) -> CountryCode {
CountryCode::parse(s).expect("valid country")
}
#[test]
fn valid_address() {
let a = Address::new(
"10 Downing St".to_string(),
None,
"London".to_string(),
None,
uk("SW1A 2AA"),
cc("GB"),
)
.expect("valid");
assert_eq!(a.line1(), "10 Downing St");
assert_eq!(a.city(), "London");
}
#[test]
fn valid_with_all_fields() {
let a = AddressBuilder::new()
.line1("221B Baker St")
.line2("Flat B")
.city("London")
.county("Greater London")
.postcode(uk("NW1 6XE"))
.country(cc("GB"))
.build()
.expect("valid");
assert_eq!(a.line2.as_deref(), Some("Flat B"));
assert_eq!(a.county.as_deref(), Some("Greater London"));
}
#[test]
fn valid_with_coords() {
let coords = Coords::new(51.5, -0.12).expect("valid coords");
let a = Address::new_with_coords(
"10 Downing St".to_string(),
None,
"London".to_string(),
None,
uk("SW1A 2AA"),
cc("GB"),
Some(coords),
)
.expect("valid");
assert_eq!(a.coords, Some(coords));
}
#[test]
fn invalid_empty_line1() {
let res = Address::new(
" ".to_string(),
None,
"London".to_string(),
None,
uk("SW1A 2AA"),
cc("GB"),
);
assert!(res.is_err());
}
#[test]
fn invalid_empty_city() {
let res = Address::new(
"10 Downing St".to_string(),
None,
"".to_string(),
None,
uk("SW1A 2AA"),
cc("GB"),
);
assert!(res.is_err());
}
#[test]
fn invalid_line2_empty() {
let res = Address::new(
"10 Downing St".to_string(),
Some(" ".to_string()),
"London".to_string(),
None,
uk("SW1A 2AA"),
cc("GB"),
);
assert!(res.is_err());
}
#[test]
fn builder_missing_field() {
let res = AddressBuilder::new().line1("10 Downing St").city("London").build();
assert!(res.is_err());
}
}