#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use alloc::{borrow::ToOwned, format, string::String, vec, vec::Vec};
use crate::ParseError;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VCard {
name: Option<String>,
phone: Option<String>,
email: Option<String>,
organization: Option<String>,
url: Option<String>,
address: Option<String>,
}
impl VCard {
pub fn parse(s: &str) -> Result<Self, ParseError> {
let mut began = false;
let mut fn_name = None;
let mut n_name = None;
let mut phone = None;
let mut email = None;
let mut organization = None;
let mut url = None;
let mut address = None;
for line in unfold(s) {
let Some((prop, value)) = line.split_once(':') else {
continue; };
let key = prop.split(';').next().unwrap_or("").to_ascii_uppercase();
match key.as_str() {
"BEGIN" if value.eq_ignore_ascii_case("VCARD") => began = true,
"FN" if fn_name.is_none() => fn_name = Some(value.to_owned()),
"N" if n_name.is_none() => n_name = Some(parse_n(value)),
"TEL" if phone.is_none() => phone = Some(value.to_owned()),
"EMAIL" if email.is_none() => email = Some(value.to_owned()),
"ORG" if organization.is_none() => organization = Some(parse_semicolons(value)),
"URL" if url.is_none() => url = Some(value.to_owned()),
"ADR" if address.is_none() => address = Some(value.to_owned()),
_ => {}
}
}
if !began {
return Err(ParseError::InvalidFormat);
}
Ok(Self { name: fn_name.or(n_name), phone, email, organization, url, address })
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
pub fn phone(&self) -> Option<&str> {
self.phone.as_deref()
}
#[must_use]
pub fn email(&self) -> Option<&str> {
self.email.as_deref()
}
#[must_use]
pub fn organization(&self) -> Option<&str> {
self.organization.as_deref()
}
#[must_use]
pub fn url(&self) -> Option<&str> {
self.url.as_deref()
}
#[must_use]
pub fn address(&self) -> Option<&str> {
self.address.as_deref()
}
}
pub fn encode_vcard(name: &str, phone: &str, email: &str) -> String {
format!("BEGIN:VCARD\r\nVERSION:3.0\r\nFN:{name}\r\nTEL:{phone}\r\nEMAIL:{email}\r\nEND:VCARD\r\n")
}
fn unfold(s: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for raw in s.split('\n') {
let line = raw.strip_suffix('\r').unwrap_or(raw);
if (line.starts_with(' ') || line.starts_with('\t')) && out.last().is_some() {
if let Some(last) = out.last_mut() {
last.push_str(&line[1..]);
}
} else {
out.push(line.to_owned());
}
}
out
}
fn parse_n(value: &str) -> String {
let parts: Vec<&str> = value.split(';').filter(|p| !p.is_empty()).collect();
parts.join(" ")
}
fn parse_semicolons(value: &str) -> String {
value.split(';').filter(|p| !p.is_empty()).collect::<Vec<_>>().join("; ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_minimal_card() {
let payload = encode_vcard("John Doe", "+1234567890", "john@example.com");
let card = VCard::parse(&payload).unwrap();
assert_eq!(card.name(), Some("John Doe"));
assert_eq!(card.phone(), Some("+1234567890"));
assert_eq!(card.email(), Some("john@example.com"));
assert_eq!(card.organization(), None);
}
#[test]
fn parse_vcard4_with_params_and_lf() {
let s = "BEGIN:VCARD\nVERSION:4.0\nFN:Jane Roe\nTEL;TYPE=cell:+15551234\nEMAIL:jane@example.org\nORG:Acme;Widgets\nURL:https://example.org\nADR;TYPE=home:;;123 Main St;Springfield;IL;62701;USA\nEND:VCARD\n";
let card = VCard::parse(s).unwrap();
assert_eq!(card.name(), Some("Jane Roe"));
assert_eq!(card.phone(), Some("+15551234"));
assert_eq!(card.email(), Some("jane@example.org"));
assert_eq!(card.organization(), Some("Acme; Widgets"));
assert_eq!(card.url(), Some("https://example.org"));
assert_eq!(card.address(), Some(";;123 Main St;Springfield;IL;62701;USA"));
}
#[test]
fn name_falls_back_to_structured_n() {
let s = "BEGIN:VCARD\nVERSION:3.0\nN:Doe;John;;;Jr\nEND:VCARD\n";
let card = VCard::parse(s).unwrap();
assert_eq!(card.name(), Some("Doe John Jr"));
}
#[test]
fn unfolds_folded_lines() {
let s = "BEGIN:VCARD\nVERSION:3.0\nFN:Fold\nURL:https://exa\n mple.org/x\nEND:VCARD\n";
let card = VCard::parse(s).unwrap();
assert_eq!(card.url(), Some("https://example.org/x"));
}
#[test]
fn missing_begin_errors() {
assert_eq!(VCard::parse("VERSION:3.0\nFN:Nope\n"), Err(ParseError::InvalidFormat));
}
}