#![forbid(unsafe_code)]
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Name {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Vec<NameComponent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_ordered: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_separator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub full: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_as: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phonetic_script: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phonetic_system: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NameComponent {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub value: String,
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub phonetic: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Nickname {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Organization {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub units: Option<Vec<OrgUnit>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_as: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgUnit {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_as: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpeakToAs {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grammatical_gender: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pronouns: Option<HashMap<String, Pronouns>>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Pronouns {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub pronouns: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Title {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_id: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmailAddress {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlineService {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Phone {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub number: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub features: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LanguagePref {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub language: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Calendar {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SchedulingAddress {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Address {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Vec<AddressComponent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_ordered: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub coordinates: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_zone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub full: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_separator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phonetic_script: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phonetic_system: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddressComponent {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub value: String,
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub phonetic: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CryptoKey {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Directory {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub list_as: Option<u32>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Link {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Media {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pref: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PartialDate {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub year: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub month: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub day: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub calendar_scale: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Timestamp {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub utc: String,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AnniversaryDate {
PartialDate(PartialDate),
Timestamp(Timestamp),
Unknown(serde_json::Value),
}
impl Serialize for AnniversaryDate {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self {
AnniversaryDate::PartialDate(d) => d.serialize(s),
AnniversaryDate::Timestamp(t) if t.at_type.is_none() => {
let mut stamped = t.clone();
stamped.at_type = Some("Timestamp".to_owned());
stamped.serialize(s)
}
AnniversaryDate::Timestamp(t) => t.serialize(s),
AnniversaryDate::Unknown(v) => v.serialize(s),
}
}
}
impl<'de> Deserialize<'de> for AnniversaryDate {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let v = serde_json::Value::deserialize(d)?;
if !v.is_object() {
return Ok(AnniversaryDate::Unknown(v));
}
match v.get("@type").and_then(|t| t.as_str()) {
None | Some("PartialDate") => {
let d: PartialDate = serde_json::from_value(v).map_err(serde::de::Error::custom)?;
Ok(AnniversaryDate::PartialDate(d))
}
Some("Timestamp") => {
let t: Timestamp = serde_json::from_value(v).map_err(serde::de::Error::custom)?;
Ok(AnniversaryDate::Timestamp(t))
}
Some(_) => Ok(AnniversaryDate::Unknown(v)),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Anniversary {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub kind: String,
pub date: AnniversaryDate,
#[serde(skip_serializing_if = "Option::is_none")]
pub place: Option<Address>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Note {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub note: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub created: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<Author>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Author {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonalInfo {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
pub kind: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub list_as: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Relation {
#[serde(rename = "@type", default, skip_serializing_if = "Option::is_none")]
pub at_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relation: Option<HashMap<String, bool>>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn assert_roundtrip<T>(value: serde_json::Value)
where
T: serde::de::DeserializeOwned + Serialize,
{
let typed: T = serde_json::from_value(value.clone())
.unwrap_or_else(|e| panic!("deserialize failed: {e}\ninput: {value}"));
let back = serde_json::to_value(&typed).unwrap_or_else(|e| panic!("serialize failed: {e}"));
assert_eq!(back, value, "round-trip mismatch");
}
#[test]
fn name_roundtrip_figure_16() {
let v = json!({
"components": [
{ "kind": "given", "value": "Vincent" },
{ "kind": "surname", "value": "van Gogh" }
],
"isOrdered": true
});
assert_roundtrip::<Name>(v);
}
#[test]
fn name_roundtrip_figure_19() {
let v = json!({
"components": [
{ "kind": "given", "value": "Robert" },
{ "kind": "given2", "value": "Pau" },
{ "kind": "surname", "value": "Shou Chang" }
],
"sortAs": {
"surname": "Pau Shou Chang",
"given": "Robert"
},
"isOrdered": true
});
assert_roundtrip::<Name>(v);
}
#[test]
fn nickname_roundtrip_figure_21() {
let v = json!({
"name": "Johnny"
});
assert_roundtrip::<Nickname>(v);
}
#[test]
fn organization_roundtrip_figure_22() {
let v = json!({
"name": "ABC, Inc.",
"units": [
{ "name": "North American Division" },
{ "name": "Marketing" }
],
"sortAs": "ABC"
});
assert_roundtrip::<Organization>(v);
}
#[test]
fn speak_to_as_roundtrip_figure_23() {
let v = json!({
"grammaticalGender": "neuter",
"pronouns": {
"k19": {
"pronouns": "they/them",
"pref": 2
},
"k32": {
"pronouns": "xe/xir",
"pref": 1
}
}
});
assert_roundtrip::<SpeakToAs>(v);
}
#[test]
fn title_roundtrip_figure_24_title() {
let v = json!({
"kind": "title",
"name": "Research Scientist"
});
assert_roundtrip::<Title>(v);
}
#[test]
fn title_roundtrip_figure_24_role() {
let v = json!({
"kind": "role",
"name": "Project Leader",
"organizationId": "o2"
});
assert_roundtrip::<Title>(v);
}
#[test]
fn email_address_roundtrip_figure_25_work() {
let v = json!({
"contexts": { "work": true },
"address": "jqpublic@xyz.example.com"
});
assert_roundtrip::<EmailAddress>(v);
}
#[test]
fn email_address_roundtrip_figure_25_pref() {
let v = json!({
"address": "jane_doe@example.com",
"pref": 1
});
assert_roundtrip::<EmailAddress>(v);
}
#[test]
fn online_service_roundtrip_figure_26() {
let v = json!({
"service": "Mastodon",
"user": "@alice@example2.com",
"uri": "https://example2.com/@alice"
});
assert_roundtrip::<OnlineService>(v);
}
#[test]
fn phone_roundtrip_figure_27() {
let v = json!({
"contexts": { "private": true },
"features": { "voice": true },
"number": "tel:+1-555-555-5555;ext=5555",
"pref": 1
});
assert_roundtrip::<Phone>(v);
}
#[test]
fn language_pref_roundtrip_figure_28() {
let v = json!({
"language": "en",
"contexts": { "work": true },
"pref": 1
});
assert_roundtrip::<LanguagePref>(v);
}
#[test]
fn calendar_roundtrip_figure_29_calendar() {
let v = json!({
"kind": "calendar",
"uri": "webcal://calendar.example.com/calA.ics"
});
assert_roundtrip::<Calendar>(v);
}
#[test]
fn calendar_roundtrip_figure_29_freebusy() {
let v = json!({
"kind": "freeBusy",
"uri": "https://calendar.example.com/busy/project-a"
});
assert_roundtrip::<Calendar>(v);
}
#[test]
fn scheduling_address_roundtrip_figure_30() {
let v = json!({
"uri": "mailto:janedoe@example.com"
});
assert_roundtrip::<SchedulingAddress>(v);
}
#[test]
fn address_roundtrip_figure_31() {
let v = json!({
"contexts": { "work": true },
"components": [
{ "kind": "number", "value": "54321" },
{ "kind": "separator", "value": " " },
{ "kind": "name", "value": "Oak St" },
{ "kind": "locality", "value": "Reston" },
{ "kind": "region", "value": "VA" },
{ "kind": "separator", "value": " " },
{ "kind": "postcode", "value": "20190" },
{ "kind": "country", "value": "USA" }
],
"countryCode": "US",
"defaultSeparator": ", ",
"isOrdered": true
});
assert_roundtrip::<Address>(v);
}
#[test]
fn crypto_key_roundtrip_figure_34() {
let v = json!({
"uri": "https://www.example.com/keys/jdoe.cer"
});
assert_roundtrip::<CryptoKey>(v);
}
#[test]
fn directory_roundtrip_figure_36_entry() {
let v = json!({
"kind": "entry",
"uri": "https://dir.example.com/addrbook/jdoe/Jean%20Dupont.vcf"
});
assert_roundtrip::<Directory>(v);
}
#[test]
fn directory_roundtrip_figure_36_directory() {
let v = json!({
"kind": "directory",
"uri": "ldap://ldap.example/o=Example%20Tech,ou=Engineering",
"pref": 1
});
assert_roundtrip::<Directory>(v);
}
#[test]
fn link_roundtrip_figure_37() {
let v = json!({
"kind": "contact",
"uri": "mailto:contact@example.com",
"pref": 1
});
assert_roundtrip::<Link>(v);
}
#[test]
fn media_roundtrip_figure_38_sound() {
let v = json!({
"kind": "sound",
"uri": "CID:JOHNQ.part8.19960229T080000.xyzMail@example.com"
});
assert_roundtrip::<Media>(v);
}
#[test]
fn media_roundtrip_figure_38_logo() {
let v = json!({
"kind": "logo",
"uri": "https://www.example.com/pub/logos/abccorp.jpg"
});
assert_roundtrip::<Media>(v);
}
#[test]
fn anniversary_roundtrip_figure_41_partial_date() {
let v = json!({
"kind": "birth",
"date": {
"year": 1953,
"month": 4,
"day": 15
}
});
assert_roundtrip::<Anniversary>(v);
}
#[test]
fn anniversary_roundtrip_figure_41_timestamp() {
let v = json!({
"kind": "death",
"date": {
"@type": "Timestamp",
"utc": "2019-10-15T23:10:00Z"
},
"place": {
"full": "4445 Tree Street\nNew England, ND 58647\nUSA"
}
});
assert_roundtrip::<Anniversary>(v);
}
#[test]
fn anniversary_date_unknown_preserves_opaque() {
let v = json!({
"kind": "birth",
"date": {
"@type": "FuturisticDateShape",
"stardate": 41153.7
}
});
let anniv: Anniversary = serde_json::from_value(v.clone()).unwrap();
assert!(matches!(anniv.date, AnniversaryDate::Unknown(_)));
let back = serde_json::to_value(&anniv).unwrap();
assert_eq!(back, v);
}
#[test]
fn anniversary_date_unknown_non_object_round_trips() {
for value in [
serde_json::Value::String("opaque-scalar".into()),
serde_json::json!([1, 2, 3]),
serde_json::Value::Null,
] {
let original = AnniversaryDate::Unknown(value.clone());
let on_wire = serde_json::to_value(&original).unwrap();
assert_eq!(on_wire, value, "serialize forwards verbatim");
let back: AnniversaryDate = serde_json::from_value(on_wire).unwrap();
assert!(
matches!(back, AnniversaryDate::Unknown(_)),
"non-object Value must deserialize back to Unknown, got: {back:?}",
);
let AnniversaryDate::Unknown(inner) = back else {
unreachable!("matches! above already guards this")
};
assert_eq!(inner, value, "Unknown payload preserved across round trip");
}
}
#[test]
fn anniversary_date_unknown_object_with_recognised_at_type_loses_variant_identity() {
let cases: &[(&str, serde_json::Value, fn(&AnniversaryDate) -> bool)] = &[
(
"object with @type = PartialDate",
serde_json::json!({"@type": "PartialDate", "year": 2000}),
|d| matches!(d, AnniversaryDate::PartialDate(_)),
),
(
"object with @type = Timestamp",
serde_json::json!({"@type": "Timestamp", "utc": "2000-01-01T00:00:00Z"}),
|d| matches!(d, AnniversaryDate::Timestamp(_)),
),
(
"object with no @type (defaultType = PartialDate)",
serde_json::json!({"year": 2000}),
|d| matches!(d, AnniversaryDate::PartialDate(_)),
),
];
for (label, value, expected) in cases {
let original = AnniversaryDate::Unknown(value.clone());
let wire = serde_json::to_value(&original).expect("serialize");
assert_eq!(&wire, value, "{label}: serialize forwards verbatim");
let back: AnniversaryDate = serde_json::from_value(wire).expect("deserialize");
assert!(
expected(&back),
"{label}: contract — recognised @type re-dispatches off Unknown, got {back:?}",
);
}
}
#[test]
fn anniversary_date_timestamp_at_type_none_round_trips_as_timestamp() {
let original = AnniversaryDate::Timestamp(Timestamp {
at_type: None,
utc: "2022-05-22T03:30:00Z".to_owned(),
extra: serde_json::Map::new(),
});
let wire = serde_json::to_value(&original).expect("serialize");
assert_eq!(
wire.get("@type").and_then(|t| t.as_str()),
Some("Timestamp"),
"Serialize must re-stamp @type when caller left it None: {wire}",
);
let back: AnniversaryDate = serde_json::from_value(wire).expect("deserialize");
match back {
AnniversaryDate::Timestamp(t) => {
assert_eq!(t.utc, "2022-05-22T03:30:00Z");
assert!(t.extra.is_empty(), "no stray flatten-extras");
}
other => panic!("variant identity lost: expected Timestamp, got {other:?}"),
}
}
#[test]
fn anniversary_date_timestamp_preserves_caller_at_type() {
let original = AnniversaryDate::Timestamp(Timestamp {
at_type: Some("Timestamp".to_owned()),
utc: "2022-05-22T03:30:00Z".to_owned(),
extra: serde_json::Map::new(),
});
let wire = serde_json::to_value(&original).expect("serialize");
assert_eq!(
wire.get("@type").and_then(|t| t.as_str()),
Some("Timestamp"),
);
let back: AnniversaryDate = serde_json::from_value(wire).expect("deserialize");
assert!(matches!(back, AnniversaryDate::Timestamp(_)));
}
#[test]
fn note_roundtrip_figure_43() {
let v = json!({
"note": "Open office hours are 1600 to 1715 EST, Mon-Fri",
"created": "2022-11-23T15:01:32Z",
"author": {
"name": "John"
}
});
assert_roundtrip::<Note>(v);
}
#[test]
fn personal_info_roundtrip_figure_44_expertise() {
let v = json!({
"kind": "expertise",
"value": "chemistry",
"level": "high"
});
assert_roundtrip::<PersonalInfo>(v);
}
#[test]
fn personal_info_roundtrip_figure_44_hobby() {
let v = json!({
"kind": "hobby",
"value": "reading",
"level": "high"
});
assert_roundtrip::<PersonalInfo>(v);
}
#[test]
fn relation_roundtrip_figure_13_friend() {
let v = json!({
"relation": { "friend": true }
});
assert_roundtrip::<Relation>(v);
}
#[test]
fn relation_roundtrip_figure_13_empty() {
let v = json!({
"relation": {}
});
assert_roundtrip::<Relation>(v);
}
fn assert_extras_roundtrip<T>(
mut raw: serde_json::Value,
vendor_key: &str,
vendor_val: serde_json::Value,
) where
T: serde::de::DeserializeOwned + Serialize,
{
raw[vendor_key] = vendor_val;
let de: T = serde_json::from_value(raw.clone()).unwrap();
let back = serde_json::to_value(&de).unwrap();
assert_eq!(
back, raw,
"full round-trip must preserve typed fields AND the vendor extra"
);
}
#[test]
fn name_preserves_vendor_extras() {
assert_extras_roundtrip::<Name>(
json!({"full": "Alice"}),
"acmeCorpNameSource",
json!("hr"),
);
}
#[test]
fn name_at_type_populates_at_type_not_extras() {
let wire = json!({"@type": "Name", "full": "Alice"});
let de: Name = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(
de.at_type.as_deref(),
Some("Name"),
"@type must populate at_type, not flow into extras"
);
assert!(
de.extra.is_empty(),
"@type must NOT leak into extras; found: {:?}",
de.extra
);
let back = serde_json::to_value(&de).unwrap();
assert_eq!(back, wire, "round-trip must preserve @type on the wire");
}
#[test]
fn name_at_type_and_vendor_extras_coexist_separately() {
let wire = json!({
"@type": "Name",
"full": "Alice",
"acmeCorpNameSource": "hr",
});
let de: Name = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(de.at_type.as_deref(), Some("Name"));
assert_eq!(
de.extra.get("acmeCorpNameSource"),
Some(&json!("hr")),
"vendor key must land in extras"
);
assert!(
!de.extra.contains_key("@type"),
"@type must NOT also appear in extras; found: {:?}",
de.extra
);
let back = serde_json::to_value(&de).unwrap();
assert_eq!(back, wire);
}
#[test]
fn nickname_preserves_vendor_extras() {
assert_extras_roundtrip::<Nickname>(
json!({"name": "Al"}),
"acmeCorpScope",
json!("internal"),
);
}
#[test]
fn organization_preserves_vendor_extras() {
assert_extras_roundtrip::<Organization>(
json!({"name": "Acme"}),
"acmeCorpDept",
json!("eng"),
);
}
#[test]
fn org_unit_preserves_vendor_extras() {
assert_extras_roundtrip::<OrgUnit>(
json!({"name": "Platform"}),
"acmeCorpCostCenter",
json!("cc-42"),
);
}
#[test]
fn speak_to_as_preserves_vendor_extras() {
assert_extras_roundtrip::<SpeakToAs>(
json!({"grammaticalGender": "feminine"}),
"acmeCorpFormality",
json!("informal"),
);
}
#[test]
fn pronouns_preserves_vendor_extras() {
assert_extras_roundtrip::<Pronouns>(
json!({"pronouns": "she/her"}),
"acmeCorpAccessibilityHint",
json!("screen-reader"),
);
}
#[test]
fn title_preserves_vendor_extras() {
assert_extras_roundtrip::<Title>(json!({"name": "Engineer"}), "acmeCorpLevel", json!(5));
}
#[test]
fn email_address_preserves_vendor_extras() {
assert_extras_roundtrip::<EmailAddress>(
json!({"address": "alice@example.com"}),
"acmeCorpVerified",
json!(true),
);
}
#[test]
fn online_service_preserves_vendor_extras() {
assert_extras_roundtrip::<OnlineService>(
json!({"service": "GitHub", "uri": "https://github.com/alice"}),
"acmeCorpScore",
json!(0.95),
);
}
#[test]
fn phone_preserves_vendor_extras() {
assert_extras_roundtrip::<Phone>(
json!({"number": "tel:+1-555-0100"}),
"acmeCorpRegion",
json!("us"),
);
}
#[test]
fn language_pref_preserves_vendor_extras() {
assert_extras_roundtrip::<LanguagePref>(
json!({"language": "en"}),
"acmeCorpProficiency",
json!("native"),
);
}
#[test]
fn calendar_preserves_vendor_extras() {
assert_extras_roundtrip::<Calendar>(
json!({"kind": "calendar", "uri": "https://cal/example"}),
"acmeCorpAccessLevel",
json!("read-only"),
);
}
#[test]
fn scheduling_address_preserves_vendor_extras() {
assert_extras_roundtrip::<SchedulingAddress>(
json!({"uri": "mailto:alice@example.com"}),
"acmeCorpReplyHint",
json!("auto"),
);
}
#[test]
fn address_preserves_vendor_extras() {
assert_extras_roundtrip::<Address>(
json!({"full": "123 Main St"}),
"acmeCorpGeocoded",
json!(true),
);
}
#[test]
fn crypto_key_preserves_vendor_extras() {
assert_extras_roundtrip::<CryptoKey>(
json!({"uri": "https://example.com/key.pem"}),
"acmeCorpKeyAlgorithm",
json!("rsa-2048"),
);
}
#[test]
fn directory_preserves_vendor_extras() {
assert_extras_roundtrip::<Directory>(
json!({"kind": "directory", "uri": "ldap://example.com"}),
"acmeCorpDirectoryNamespace",
json!("internal"),
);
}
#[test]
fn link_preserves_vendor_extras() {
assert_extras_roundtrip::<Link>(
json!({"uri": "https://example.com"}),
"acmeCorpLinkRel",
json!("homepage"),
);
}
#[test]
fn media_preserves_vendor_extras() {
assert_extras_roundtrip::<Media>(
json!({"kind": "photo", "uri": "https://example.com/photo.jpg"}),
"acmeCorpThumbnailUri",
json!("https://example.com/photo.thumb.jpg"),
);
}
#[test]
fn anniversary_preserves_vendor_extras() {
assert_extras_roundtrip::<Anniversary>(
json!({"kind": "birth", "date": {"year": 2000, "month": 1, "day": 1}}),
"acmeCorpReminderDays",
json!(7),
);
}
#[test]
fn note_preserves_vendor_extras() {
assert_extras_roundtrip::<Note>(
json!({"note": "important"}),
"acmeCorpClassification",
json!("internal"),
);
}
#[test]
fn author_preserves_vendor_extras() {
assert_extras_roundtrip::<Author>(
json!({"name": "Alice"}),
"acmeCorpAuthorRole",
json!("manager"),
);
}
#[test]
fn personal_info_preserves_vendor_extras() {
assert_extras_roundtrip::<PersonalInfo>(
json!({"kind": "hobby", "value": "skiing"}),
"acmeCorpInterestRank",
json!(3),
);
}
#[test]
fn relation_preserves_vendor_extras() {
assert_extras_roundtrip::<Relation>(
json!({"relation": {"friend": true}}),
"acmeCorpRelationStrength",
json!("close"),
);
}
#[test]
fn name_component_preserves_vendor_extras() {
assert_extras_roundtrip::<NameComponent>(
json!({"value": "Vincent", "kind": "given"}),
"acmeCorpComponentSource",
json!("hr"),
);
}
#[test]
fn address_component_preserves_vendor_extras() {
assert_extras_roundtrip::<AddressComponent>(
json!({"value": "123", "kind": "number"}),
"acmeCorpVerified",
json!(true),
);
}
#[test]
fn partial_date_preserves_vendor_extras() {
assert_extras_roundtrip::<PartialDate>(
json!({"year": 2000, "month": 1, "day": 1}),
"acmeCorpDateSource",
json!("self-reported"),
);
}
#[test]
fn timestamp_preserves_vendor_extras() {
assert_extras_roundtrip::<Timestamp>(
json!({"@type": "Timestamp", "utc": "2022-05-22T03:30:00Z"}),
"acmeCorpTimezone",
json!("UTC"),
);
}
}