use super::error::{OtomlError, Result};
use super::olocation::OLocation;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OAddressRef {
#[serde(skip_serializing_if = "Option::is_none")]
pub geonames_id: Option<u32>,
}
impl OAddressRef {
pub fn new() -> Self {
Self::default()
}
pub fn with_geonames(id: u32) -> Self {
Self {
geonames_id: Some(id),
}
}
pub fn is_empty(&self) -> bool {
self.geonames_id.is_none()
}
}
#[derive(Debug, Clone, Default)]
pub struct OAddress {
country: String,
region: Option<String>,
city: Option<String>,
postal_code: Option<String>,
street: Option<String>,
number: Option<String>,
unit: Option<String>,
location: Option<OLocation>,
refs: Option<OAddressRef>,
}
impl OAddress {
pub fn new(country: &str) -> Result<Self> {
let country = canonicalize_country(country)?;
Ok(Self {
country,
..Default::default()
})
}
pub fn builder(country: &str) -> OAddressBuilder {
OAddressBuilder::new(country)
}
pub fn country(&self) -> &str {
&self.country
}
pub fn region(&self) -> Option<&str> {
self.region.as_deref()
}
pub fn city(&self) -> Option<&str> {
self.city.as_deref()
}
pub fn postal_code(&self) -> Option<&str> {
self.postal_code.as_deref()
}
pub fn street(&self) -> Option<&str> {
self.street.as_deref()
}
pub fn number(&self) -> Option<&str> {
self.number.as_deref()
}
pub fn unit(&self) -> Option<&str> {
self.unit.as_deref()
}
pub fn location(&self) -> Option<&OLocation> {
self.location.as_ref()
}
pub fn refs(&self) -> Option<&OAddressRef> {
self.refs.as_ref()
}
pub fn is_minimal(&self) -> bool {
self.region.is_none()
&& self.city.is_none()
&& self.postal_code.is_none()
&& self.street.is_none()
&& self.number.is_none()
&& self.unit.is_none()
}
pub fn has_location(&self) -> bool {
self.location.is_some()
}
pub fn has_refs(&self) -> bool {
self.refs.as_ref().is_some_and(|r| !r.is_empty())
}
}
pub struct OAddressBuilder {
country: String,
region: Option<String>,
city: Option<String>,
postal_code: Option<String>,
street: Option<String>,
number: Option<String>,
unit: Option<String>,
location: Option<OLocation>,
refs: Option<OAddressRef>,
}
impl OAddressBuilder {
pub fn new(country: &str) -> Self {
Self {
country: country.to_string(),
region: None,
city: None,
postal_code: None,
street: None,
number: None,
unit: None,
location: None,
refs: None,
}
}
pub fn region(mut self, region: &str) -> Self {
self.region = Some(region.to_string());
self
}
pub fn city(mut self, city: &str) -> Self {
self.city = Some(city.to_string());
self
}
pub fn postal_code(mut self, postal_code: &str) -> Self {
self.postal_code = Some(postal_code.to_string());
self
}
pub fn street(mut self, street: &str) -> Self {
self.street = Some(street.to_string());
self
}
pub fn number(mut self, number: &str) -> Self {
self.number = Some(number.to_string());
self
}
pub fn unit(mut self, unit: &str) -> Self {
self.unit = Some(unit.to_string());
self
}
pub fn location(mut self, location: OLocation) -> Self {
self.location = Some(location);
self
}
pub fn refs(mut self, refs: OAddressRef) -> Self {
self.refs = Some(refs);
self
}
pub fn geonames_id(mut self, id: u32) -> Self {
self.refs = Some(OAddressRef::with_geonames(id));
self
}
pub fn build(self) -> Result<OAddress> {
let country = canonicalize_country(&self.country)?;
let region = self.region.map(|s| canonicalize_string(&s)).transpose()?;
let city = self.city.map(|s| canonicalize_string(&s)).transpose()?;
let postal_code = self
.postal_code
.map(|s| canonicalize_postal_code(&s))
.transpose()?;
let street = self.street.map(|s| canonicalize_string(&s)).transpose()?;
let number = self.number.map(|s| canonicalize_string(&s)).transpose()?;
let unit = self.unit.map(|s| canonicalize_string(&s)).transpose()?;
Ok(OAddress {
country,
region,
city,
postal_code,
street,
number,
unit,
location: self.location,
refs: self.refs,
})
}
}
impl PartialEq for OAddress {
fn eq(&self, other: &Self) -> bool {
self.country == other.country
&& self.region == other.region
&& self.city == other.city
&& self.postal_code == other.postal_code
&& self.street == other.street
&& self.number == other.number
&& self.unit == other.unit
&& self.location == other.location
}
}
impl Eq for OAddress {}
impl Hash for OAddress {
fn hash<H: Hasher>(&self, state: &mut H) {
self.country.hash(state);
self.region.hash(state);
self.city.hash(state);
self.postal_code.hash(state);
self.street.hash(state);
self.number.hash(state);
self.unit.hash(state);
self.location.hash(state);
}
}
impl Ord for OAddress {
fn cmp(&self, other: &Self) -> Ordering {
self.country
.cmp(&other.country)
.then_with(|| self.region.cmp(&other.region))
.then_with(|| self.city.cmp(&other.city))
.then_with(|| self.postal_code.cmp(&other.postal_code))
.then_with(|| self.street.cmp(&other.street))
.then_with(|| self.number.cmp(&other.number))
.then_with(|| self.unit.cmp(&other.unit))
}
}
impl PartialOrd for OAddress {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for OAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{{ country: \"{}\"", self.country)?;
if let Some(ref region) = self.region {
write!(f, ", region: \"{}\"", region)?;
}
if let Some(ref city) = self.city {
write!(f, ", city: \"{}\"", city)?;
}
if let Some(ref postal_code) = self.postal_code {
write!(f, ", postal_code: \"{}\"", postal_code)?;
}
if let Some(ref street) = self.street {
write!(f, ", street: \"{}\"", street)?;
}
if let Some(ref number) = self.number {
write!(f, ", number: \"{}\"", number)?;
}
if let Some(ref unit) = self.unit {
write!(f, ", unit: \"{}\"", unit)?;
}
if let Some(ref location) = self.location {
write!(f, ", location: {}", location)?;
}
if let Some(ref refs) = self.refs {
if let Some(id) = refs.geonames_id {
write!(f, ", refs: {{ geonames_id: {} }}", id)?;
}
}
write!(f, " }}")
}
}
#[derive(Serialize, Deserialize)]
struct OAddressSerde {
country: String,
#[serde(skip_serializing_if = "Option::is_none")]
region: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
city: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
postal_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
street: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
number: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
unit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
location: Option<OLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
refs: Option<OAddressRef>,
}
impl Serialize for OAddress {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let serde_repr = OAddressSerde {
country: self.country.clone(),
region: self.region.clone(),
city: self.city.clone(),
postal_code: self.postal_code.clone(),
street: self.street.clone(),
number: self.number.clone(),
unit: self.unit.clone(),
location: self.location,
refs: self.refs.clone(),
};
serde_repr.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for OAddress {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let serde_repr = OAddressSerde::deserialize(deserializer)?;
let country =
canonicalize_country(&serde_repr.country).map_err(serde::de::Error::custom)?;
let region = serde_repr
.region
.map(|s| canonicalize_string(&s))
.transpose()
.map_err(serde::de::Error::custom)?;
let city = serde_repr
.city
.map(|s| canonicalize_string(&s))
.transpose()
.map_err(serde::de::Error::custom)?;
let postal_code = serde_repr
.postal_code
.map(|s| canonicalize_postal_code(&s))
.transpose()
.map_err(serde::de::Error::custom)?;
let street = serde_repr
.street
.map(|s| canonicalize_string(&s))
.transpose()
.map_err(serde::de::Error::custom)?;
let number = serde_repr
.number
.map(|s| canonicalize_string(&s))
.transpose()
.map_err(serde::de::Error::custom)?;
let unit = serde_repr
.unit
.map(|s| canonicalize_string(&s))
.transpose()
.map_err(serde::de::Error::custom)?;
Ok(OAddress {
country,
region,
city,
postal_code,
street,
number,
unit,
location: serde_repr.location,
refs: serde_repr.refs,
})
}
}
fn canonicalize_country(s: &str) -> Result<String> {
let s = s.trim().to_lowercase();
if s.is_empty() {
return Err(OtomlError::InvalidAddress(
"country code cannot be empty".to_string(),
));
}
if s.len() != 2 {
return Err(OtomlError::InvalidAddress(format!(
"country code must be 2 characters, got '{}'",
s
)));
}
if !s.chars().all(|c| c.is_ascii_lowercase()) {
return Err(OtomlError::InvalidAddress(format!(
"country code must be ASCII letters only, got '{}'",
s
)));
}
Ok(s)
}
fn canonicalize_string(s: &str) -> Result<String> {
let s = s.trim().to_lowercase();
if s.is_empty() {
return Err(OtomlError::InvalidAddress(
"field cannot be empty string".to_string(),
));
}
Ok(s)
}
fn canonicalize_postal_code(s: &str) -> Result<String> {
let s = s.trim().to_lowercase();
if s.is_empty() {
return Err(OtomlError::InvalidAddress(
"postal code cannot be empty".to_string(),
));
}
let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
Ok(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_minimal_address() {
let addr = OAddress::new("ch").unwrap();
assert_eq!(addr.country(), "ch");
assert!(addr.is_minimal());
assert!(!addr.has_location());
assert!(!addr.has_refs());
}
#[test]
fn test_builder() {
let addr = OAddress::builder("ch")
.region("zh")
.city("zurich")
.postal_code("8001")
.street("bahnstrasse")
.number("12")
.unit("apt_4b")
.build()
.unwrap();
assert_eq!(addr.country(), "ch");
assert_eq!(addr.region(), Some("zh"));
assert_eq!(addr.city(), Some("zurich"));
assert_eq!(addr.postal_code(), Some("8001"));
assert_eq!(addr.street(), Some("bahnstrasse"));
assert_eq!(addr.number(), Some("12"));
assert_eq!(addr.unit(), Some("apt_4b"));
}
#[test]
fn test_country_validation() {
assert!(OAddress::new("ch").is_ok());
assert!(OAddress::new("us").is_ok());
assert!(OAddress::new("DE").is_ok());
assert!(OAddress::new("").is_err());
assert!(OAddress::new("a").is_err());
assert!(OAddress::new("abc").is_err());
assert!(OAddress::new("12").is_err());
}
#[test]
fn test_canonicalization() {
let addr = OAddress::builder("CH")
.city("Zurich")
.postal_code(" 8001 ")
.street("Bahnstrasse")
.build()
.unwrap();
assert_eq!(addr.country(), "ch");
assert_eq!(addr.city(), Some("zurich"));
assert_eq!(addr.postal_code(), Some("8001"));
assert_eq!(addr.street(), Some("bahnstrasse"));
}
#[test]
fn test_equality_ignores_refs() {
let addr1 = OAddress::builder("ch")
.city("zurich")
.geonames_id(2657896)
.build()
.unwrap();
let addr2 = OAddress::builder("ch").city("zurich").build().unwrap();
assert_eq!(addr1, addr2);
}
#[test]
fn test_ordering() {
let ch = OAddress::new("ch").unwrap();
let de = OAddress::new("de").unwrap();
let us = OAddress::new("us").unwrap();
assert!(ch < de);
assert!(de < us);
}
#[test]
fn test_ordering_with_city() {
let zurich = OAddress::builder("ch").city("zurich").build().unwrap();
let bern = OAddress::builder("ch").city("bern").build().unwrap();
assert!(bern < zurich);
}
#[test]
fn test_with_location() {
let loc = OLocation::new(47.376887, 8.541694, 20).unwrap();
let addr = OAddress::builder("ch")
.city("zurich")
.location(loc)
.build()
.unwrap();
assert!(addr.has_location());
assert_eq!(addr.location().unwrap().lat(), loc.lat());
}
#[test]
fn test_with_refs() {
let addr = OAddress::builder("ch")
.city("zurich")
.geonames_id(2657896)
.build()
.unwrap();
assert!(addr.has_refs());
assert_eq!(addr.refs().unwrap().geonames_id, Some(2657896));
}
#[test]
fn test_display() {
let addr = OAddress::builder("ch")
.city("zurich")
.postal_code("8001")
.build()
.unwrap();
let s = addr.to_string();
assert!(s.contains("country: \"ch\""));
assert!(s.contains("city: \"zurich\""));
assert!(s.contains("postal_code: \"8001\""));
}
#[test]
fn test_serde_roundtrip() {
let addr = OAddress::builder("ch")
.region("zh")
.city("zurich")
.postal_code("8001")
.street("bahnstrasse")
.number("12")
.build()
.unwrap();
let otoml = crate::dump_otoml(&addr).unwrap();
let parsed: OAddress = crate::load_otoml(&otoml).unwrap();
assert_eq!(addr, parsed);
}
#[test]
fn test_binary_roundtrip() {
let addr = OAddress::builder("ch")
.city("zurich")
.postal_code("8001")
.build()
.unwrap();
let bytes = crate::dump_obin(&addr).unwrap();
let parsed: OAddress = crate::load_obin(&bytes).unwrap();
assert_eq!(addr, parsed);
}
#[test]
fn test_with_location_serde() {
let loc = OLocation::new(47.376887, 8.541694, 20).unwrap();
let addr = OAddress::builder("ch")
.city("zurich")
.location(loc)
.geonames_id(2657896)
.build()
.unwrap();
let otoml = crate::dump_otoml(&addr).unwrap();
let parsed: OAddress = crate::load_otoml(&otoml).unwrap();
assert_eq!(addr, parsed);
assert!(parsed.has_location());
assert!(parsed.has_refs());
}
#[test]
fn test_hash_ignores_refs() {
use std::collections::hash_map::DefaultHasher;
let addr1 = OAddress::builder("ch")
.city("zurich")
.geonames_id(2657896)
.build()
.unwrap();
let addr2 = OAddress::builder("ch").city("zurich").build().unwrap();
let mut hasher1 = DefaultHasher::new();
let mut hasher2 = DefaultHasher::new();
addr1.hash(&mut hasher1);
addr2.hash(&mut hasher2);
assert_eq!(hasher1.finish(), hasher2.finish());
}
#[test]
fn test_postal_code_formats() {
let ch = OAddress::builder("ch").postal_code("8001").build().unwrap();
assert_eq!(ch.postal_code(), Some("8001"));
let us = OAddress::builder("us")
.postal_code("94105")
.build()
.unwrap();
assert_eq!(us.postal_code(), Some("94105"));
let uk = OAddress::builder("gb")
.postal_code("SW1A 1AA")
.build()
.unwrap();
assert_eq!(uk.postal_code(), Some("sw1a1aa"));
let jp = OAddress::builder("jp")
.postal_code("100-0001")
.build()
.unwrap();
assert_eq!(jp.postal_code(), Some("100-0001"));
}
#[test]
fn test_alphanumeric_building_numbers() {
let addr = OAddress::builder("gb")
.street("baker_street")
.number("221b")
.build()
.unwrap();
assert_eq!(addr.number(), Some("221b"));
let range = OAddress::builder("de")
.street("hauptstrasse")
.number("7-9")
.build()
.unwrap();
assert_eq!(range.number(), Some("7-9"));
}
#[test]
fn test_default() {
let addr = OAddress::default();
assert_eq!(addr.country(), "");
assert!(addr.is_minimal());
}
#[test]
fn test_clone() {
let addr = OAddress::builder("ch")
.city("zurich")
.postal_code("8001")
.build()
.unwrap();
let cloned = addr.clone();
assert_eq!(addr, cloned);
assert_eq!(addr.country(), cloned.country());
assert_eq!(addr.city(), cloned.city());
}
#[test]
fn test_all_fields() {
let loc = OLocation::new(47.376887, 8.541694, 20).unwrap();
let addr = OAddress::builder("ch")
.region("zh")
.city("zurich")
.postal_code("8001")
.street("bahnhofstrasse")
.number("1")
.unit("floor_3")
.location(loc)
.geonames_id(2657896)
.build()
.unwrap();
assert_eq!(addr.country(), "ch");
assert_eq!(addr.region(), Some("zh"));
assert_eq!(addr.city(), Some("zurich"));
assert_eq!(addr.postal_code(), Some("8001"));
assert_eq!(addr.street(), Some("bahnhofstrasse"));
assert_eq!(addr.number(), Some("1"));
assert_eq!(addr.unit(), Some("floor_3"));
assert!(addr.has_location());
assert!(addr.has_refs());
assert!(!addr.is_minimal());
}
#[test]
fn test_empty_string_fields_rejected() {
let result = OAddress::builder("ch").city("").build();
assert!(result.is_err());
let result = OAddress::builder("ch").street("").build();
assert!(result.is_err());
let result = OAddress::builder("ch").city(" ").build();
assert!(result.is_err());
}
#[test]
fn test_ordering_comprehensive() {
let mut addrs = vec![
OAddress::builder("us").city("new york").build().unwrap(),
OAddress::builder("ch").city("zurich").build().unwrap(),
OAddress::builder("ch").city("bern").build().unwrap(),
OAddress::builder("ch")
.city("zurich")
.postal_code("8001")
.build()
.unwrap(),
OAddress::new("ch").unwrap(),
];
addrs.sort();
assert_eq!(addrs[0].country(), "ch");
assert_eq!(addrs[0].city(), None); assert_eq!(addrs[1].city(), Some("bern"));
assert_eq!(addrs[2].city(), Some("zurich"));
assert_eq!(addrs[2].postal_code(), None);
assert_eq!(addrs[3].city(), Some("zurich"));
assert_eq!(addrs[3].postal_code(), Some("8001"));
assert_eq!(addrs[4].country(), "us");
}
#[test]
fn test_hash_in_hashset() {
use std::collections::HashSet;
let a1 = OAddress::builder("ch").city("zurich").build().unwrap();
let a2 = OAddress::builder("ch").city("zurich").build().unwrap();
let a3 = OAddress::builder("ch")
.city("zurich")
.geonames_id(123)
.build()
.unwrap();
let a4 = OAddress::builder("ch").city("bern").build().unwrap();
let mut set = HashSet::new();
set.insert(a1);
set.insert(a2); set.insert(a3); set.insert(a4);
assert_eq!(set.len(), 2); }
#[test]
fn test_refs_struct() {
use crate::OAddressRef;
let empty = OAddressRef::new();
assert!(empty.is_empty());
assert_eq!(empty.geonames_id, None);
let with_geo = OAddressRef::with_geonames(2657896);
assert!(!with_geo.is_empty());
assert_eq!(with_geo.geonames_id, Some(2657896));
}
#[test]
fn test_international_country_codes() {
let countries = [
"us", "ca", "mx", "br", "ar", "cl", "gb", "de", "fr", "ch", "it", "es", "cn", "jp", "kr", "in", "sg", "au", "nz", "za", "eg", "ng", ];
for code in countries {
let addr = OAddress::new(code);
assert!(addr.is_ok(), "Failed for country: {}", code);
assert_eq!(addr.unwrap().country(), code);
}
}
#[test]
fn test_deterministic_serialization() {
let addr1 = OAddress::builder("ch")
.city("zurich")
.postal_code("8001")
.street("bahnhofstrasse")
.build()
.unwrap();
let addr2 = OAddress::builder("ch")
.city("zurich")
.postal_code("8001")
.street("bahnhofstrasse")
.build()
.unwrap();
let text1 = crate::dump_otoml(&addr1).unwrap();
let text2 = crate::dump_otoml(&addr2).unwrap();
assert_eq!(text1, text2);
let bytes1 = crate::dump_obin(&addr1).unwrap();
let bytes2 = crate::dump_obin(&addr2).unwrap();
assert_eq!(bytes1, bytes2);
}
#[test]
fn test_nested_in_struct() {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Office {
name: String,
primary: OAddress,
secondary: Option<OAddress>,
}
let office = Office {
name: "HQ".to_string(),
primary: OAddress::builder("ch").city("zurich").build().unwrap(),
secondary: Some(OAddress::builder("de").city("berlin").build().unwrap()),
};
let text = crate::dump_otoml(&office).unwrap();
let parsed: Office = crate::load_otoml(&text).unwrap();
assert_eq!(office, parsed);
let bytes = crate::dump_obin(&office).unwrap();
let parsed: Office = crate::load_obin(&bytes).unwrap();
assert_eq!(office, parsed);
}
}