use serde::{Deserialize, Serialize};
use crate::visibility::Visibility;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContactEntry {
pub id: String,
pub entry_type: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handle: Option<String>,
#[serde(default)]
pub visibility: Visibility,
#[serde(skip_serializing_if = "Option::is_none")]
pub verified: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContactSection {
pub entries: Vec<ContactEntry>,
}
pub mod entry_types {
pub const PHONE: &str = "phone";
pub const EMAIL: &str = "email";
pub const SOCIAL: &str = "social";
pub const WEBSITE: &str = "website";
pub const MESSAGING: &str = "messaging";
}
pub mod platforms {
pub const X: &str = "x";
pub const LINKEDIN: &str = "linkedin";
pub const GITHUB: &str = "github";
pub const DISCORD: &str = "discord";
pub const TELEGRAM: &str = "telegram";
pub const SLACK: &str = "slack";
pub const WHATSAPP: &str = "whatsapp";
pub const SIGNAL: &str = "signal";
pub const MASTODON: &str = "mastodon";
pub const BLUESKY: &str = "bluesky";
pub const YOUTUBE: &str = "youtube";
pub const INSTAGRAM: &str = "instagram";
pub const TIKTOK: &str = "tiktok";
pub const REDDIT: &str = "reddit";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_round_trip() {
let contact = ContactSection {
entries: vec![
ContactEntry {
id: "phone-1".to_string(),
entry_type: entry_types::PHONE.to_string(),
value: "tel:+15551234567".to_string(),
label: Some("Primary".to_string()),
platform: None,
handle: None,
visibility: Visibility::Public,
verified: Some(true),
},
ContactEntry {
id: "social-x".to_string(),
entry_type: entry_types::SOCIAL.to_string(),
value: "https://x.com/agent42".to_string(),
label: None,
platform: Some(platforms::X.to_string()),
handle: Some("@agent42".to_string()),
visibility: Visibility::Public,
verified: None,
},
ContactEntry {
id: "phone-internal".to_string(),
entry_type: entry_types::PHONE.to_string(),
value: "tel:+15559876543".to_string(),
label: Some("Internal".to_string()),
platform: None,
handle: None,
visibility: Visibility::Organization,
verified: None,
},
],
};
let json = serde_json::to_string_pretty(&contact).unwrap_or_default();
let parsed: ContactSection =
serde_json::from_str(&json).unwrap_or_else(|_| contact.clone());
assert_eq!(contact, parsed);
}
#[test]
fn visibility_defaults_to_public() {
let json = r#"{"id":"e1","entryType":"email","value":"mailto:a@b.com"}"#;
let entry: ContactEntry = serde_json::from_str(json).unwrap_or_else(|_| ContactEntry {
id: String::new(),
entry_type: String::new(),
value: String::new(),
label: None,
platform: None,
handle: None,
visibility: Visibility::Private,
verified: None,
});
assert_eq!(entry.visibility, Visibility::Public);
}
#[test]
fn organization_visibility_entry() {
let entry = ContactEntry {
id: "phone-internal".to_string(),
entry_type: entry_types::PHONE.to_string(),
value: "tel:+15559876543".to_string(),
label: Some("Internal hotline".to_string()),
platform: None,
handle: None,
visibility: Visibility::Organization,
verified: None,
};
assert!(entry.visibility.is_redacted_on_dht());
}
}