use crate::errors::ValidationError;
use crate::traits::ValueObject;
use super::country_code::CountryCode;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PostalAddressInput {
pub street: String,
pub city: String,
pub zip: String,
pub country: CountryCode,
}
pub type PostalAddressOutput = String;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PostalAddress {
street: String,
city: String,
zip: String,
country: CountryCode,
#[cfg_attr(feature = "serde", serde(skip))]
formatted: String,
}
impl ValueObject for PostalAddress {
type Input = PostalAddressInput;
type Output = PostalAddressOutput;
type Error = ValidationError;
fn new(value: Self::Input) -> Result<Self, Self::Error> {
let street = value.street.trim().to_owned();
let city = value.city.trim().to_owned();
let zip = value.zip.trim().to_owned();
if street.is_empty() {
return Err(ValidationError::empty("PostalAddress.street"));
}
if city.is_empty() {
return Err(ValidationError::empty("PostalAddress.city"));
}
if zip.is_empty() {
return Err(ValidationError::empty("PostalAddress.zip"));
}
let formatted = format!("{}\n{} {}\n{}", street, zip, city, value.country.value());
Ok(Self {
street,
city,
zip,
country: value.country,
formatted,
})
}
fn value(&self) -> &Self::Output {
&self.formatted
}
fn into_inner(self) -> Self::Input {
PostalAddressInput {
street: self.street,
city: self.city,
zip: self.zip,
country: self.country,
}
}
}
impl PostalAddress {
pub fn street(&self) -> &str {
&self.street
}
pub fn city(&self) -> &str {
&self.city
}
pub fn zip(&self) -> &str {
&self.zip
}
pub fn country(&self) -> &CountryCode {
&self.country
}
}
impl std::fmt::Display for PostalAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.formatted)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::ValueObject;
fn cz() -> CountryCode {
CountryCode::new("CZ".into()).unwrap()
}
fn valid_input() -> PostalAddressInput {
PostalAddressInput {
street: "Václavské náměstí 1".into(),
city: "Prague".into(),
zip: "110 00".into(),
country: cz(),
}
}
#[test]
fn constructs_valid_address() {
let addr = PostalAddress::new(valid_input()).unwrap();
assert_eq!(addr.street(), "Václavské náměstí 1");
assert_eq!(addr.city(), "Prague");
assert_eq!(addr.zip(), "110 00");
assert_eq!(addr.country().value(), "CZ");
}
#[test]
fn value_is_formatted_string() {
let addr = PostalAddress::new(valid_input()).unwrap();
assert_eq!(addr.value(), "Václavské náměstí 1\n110 00 Prague\nCZ");
}
#[test]
fn display_matches_value() {
let addr = PostalAddress::new(valid_input()).unwrap();
assert_eq!(addr.to_string(), addr.value().to_owned());
}
#[test]
fn trims_whitespace_from_fields() {
let addr = PostalAddress::new(PostalAddressInput {
street: " Main St 1 ".into(),
city: " Berlin ".into(),
zip: " 10115 ".into(),
country: CountryCode::new("DE".into()).unwrap(),
})
.unwrap();
assert_eq!(addr.street(), "Main St 1");
assert_eq!(addr.city(), "Berlin");
assert_eq!(addr.zip(), "10115");
}
#[test]
fn rejects_empty_street() {
let mut input = valid_input();
input.street = String::new();
assert!(PostalAddress::new(input).is_err());
}
#[test]
fn rejects_whitespace_only_street() {
let mut input = valid_input();
input.street = " ".into();
assert!(PostalAddress::new(input).is_err());
}
#[test]
fn rejects_empty_city() {
let mut input = valid_input();
input.city = String::new();
assert!(PostalAddress::new(input).is_err());
}
#[test]
fn rejects_empty_zip() {
let mut input = valid_input();
input.zip = String::new();
assert!(PostalAddress::new(input).is_err());
}
#[test]
fn into_inner_returns_original_fields() {
let addr = PostalAddress::new(valid_input()).unwrap();
let inner = addr.into_inner();
assert_eq!(inner.street, "Václavské náměstí 1");
assert_eq!(inner.city, "Prague");
assert_eq!(inner.zip, "110 00");
assert_eq!(inner.country.value(), "CZ");
}
#[test]
fn equal_addresses_are_equal() {
let a = PostalAddress::new(valid_input()).unwrap();
let b = PostalAddress::new(valid_input()).unwrap();
assert_eq!(a, b);
}
}