use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StructuredName {
pub family: String,
pub given: String,
pub additional: String,
pub prefix: String,
pub suffix: String,
}
impl StructuredName {
pub fn new(family: impl Into<String>, given: impl Into<String>) -> Self {
Self {
family: family.into(),
given: given.into(),
additional: String::new(),
prefix: String::new(),
suffix: String::new(),
}
}
pub fn with_additional(mut self, additional: impl Into<String>) -> Self {
self.additional = additional.into();
self
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
self.suffix = suffix.into();
self
}
pub fn parse(s: &str) -> Self {
let parts = split_semicolons(s, 5);
Self {
family: parts.first().map(|s| unescape(s)).unwrap_or_default(),
given: parts.get(1).map(|s| unescape(s)).unwrap_or_default(),
additional: parts.get(2).map(|s| unescape(s)).unwrap_or_default(),
prefix: parts.get(3).map(|s| unescape(s)).unwrap_or_default(),
suffix: parts.get(4).map(|s| unescape(s)).unwrap_or_default(),
}
}
pub fn to_value(&self) -> String {
format!(
"{};{};{};{};{}",
escape(&self.family),
escape(&self.given),
escape(&self.additional),
escape(&self.prefix),
escape(&self.suffix)
)
}
}
impl fmt::Display for StructuredName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_value())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Address {
pub po_box: String,
pub extended: String,
pub street: String,
pub city: String,
pub region: String,
pub postal_code: String,
pub country: String,
}
impl Address {
pub fn new() -> Self {
Self::default()
}
pub fn street(mut self, street: impl Into<String>) -> Self {
self.street = street.into();
self
}
pub fn city(mut self, city: impl Into<String>) -> Self {
self.city = city.into();
self
}
pub fn region(mut self, region: impl Into<String>) -> Self {
self.region = region.into();
self
}
pub fn postal_code(mut self, postal_code: impl Into<String>) -> Self {
self.postal_code = postal_code.into();
self
}
pub fn country(mut self, country: impl Into<String>) -> Self {
self.country = country.into();
self
}
pub fn po_box(mut self, po_box: impl Into<String>) -> Self {
self.po_box = po_box.into();
self
}
pub fn extended(mut self, extended: impl Into<String>) -> Self {
self.extended = extended.into();
self
}
pub fn parse(s: &str) -> Self {
let parts = split_semicolons(s, 7);
Self {
po_box: parts.first().map(|s| unescape(s)).unwrap_or_default(),
extended: parts.get(1).map(|s| unescape(s)).unwrap_or_default(),
street: parts.get(2).map(|s| unescape(s)).unwrap_or_default(),
city: parts.get(3).map(|s| unescape(s)).unwrap_or_default(),
region: parts.get(4).map(|s| unescape(s)).unwrap_or_default(),
postal_code: parts.get(5).map(|s| unescape(s)).unwrap_or_default(),
country: parts.get(6).map(|s| unescape(s)).unwrap_or_default(),
}
}
pub fn to_value(&self) -> String {
format!(
"{};{};{};{};{};{};{}",
escape(&self.po_box),
escape(&self.extended),
escape(&self.street),
escape(&self.city),
escape(&self.region),
escape(&self.postal_code),
escape(&self.country)
)
}
}
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_value())
}
}
fn split_semicolons(s: &str, max: usize) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
current.push(ch);
if let Some(&next) = chars.peek() {
current.push(next);
chars.next();
}
} else if ch == ';' && parts.len() + 1 < max {
parts.push(current);
current = String::new();
} else {
current.push(ch);
}
}
parts.push(current);
parts
}
fn escape(s: &str) -> String {
s.replace('\\', "\\\\")
.replace(';', "\\;")
.replace(',', "\\,")
.replace('\n', "\\n")
}
fn unescape(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.peek() {
Some('n') | Some('N') => {
result.push('\n');
chars.next();
}
Some('\\') => {
result.push('\\');
chars.next();
}
Some(';') => {
result.push(';');
chars.next();
}
Some(',') => {
result.push(',');
chars.next();
}
_ => result.push('\\'),
}
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_roundtrip() {
let name = StructuredName::new("Doe", "Jane")
.with_additional("Marie")
.with_prefix("Dr.")
.with_suffix("PhD");
let value = name.to_value();
let parsed = StructuredName::parse(&value);
assert_eq!(name, parsed);
}
#[test]
fn address_roundtrip() {
let addr = Address::new()
.street("123 Main St")
.city("Anytown")
.region("CA")
.postal_code("90210")
.country("USA");
let value = addr.to_value();
let parsed = Address::parse(&value);
assert_eq!(addr, parsed);
}
#[test]
fn name_with_semicolons() {
let name = StructuredName::new("O;Brien", "Conan");
let value = name.to_value();
let parsed = StructuredName::parse(&value);
assert_eq!(parsed.family, "O;Brien");
assert_eq!(parsed.given, "Conan");
}
}