gedcomx/agent/
agent.rs

1use std::convert::TryInto;
2
3use quickcheck::{Arbitrary, Gen};
4use serde::{Deserialize, Serialize};
5use serde_with::skip_serializing_none;
6use yaserde_derive::{YaDeserialize, YaSerialize};
7
8use crate::{Address, Id, Identifier, OnlineAccount, Person, ResourceReference, Result, TextValue};
9
10/// Someone or something that curates genealogical data, such as a genealogical
11/// researcher, user of software, organization, or group.
12///
13/// In genealogical research, an agent often takes the role of a contributor.
14#[skip_serializing_none]
15#[derive(
16    Debug, Serialize, Deserialize, YaSerialize, YaDeserialize, PartialEq, Default, Clone, Eq,
17)]
18#[yaserde(
19    rename = "agent",
20    prefix = "gx",
21    default_namespace = "gx",
22    namespace = "gx: http://gedcomx.org/v1/"
23)]
24#[non_exhaustive]
25pub struct Agent {
26    /// An identifier for the data structure holding the agent data.
27    /// The id is to be used as a "fragment identifier" as defined by [RFC 3986, Section 3.5](https://tools.ietf.org/html/rfc3986#section-3.5).
28    /// As such, the constraints of the id are provided in the definition of the
29    /// media type (e.g. XML, JSON) of the data structure.
30    #[yaserde(attribute)]
31    pub id: Option<Id>,
32
33    /// A list of identifiers for the agent.
34    #[yaserde(rename = "identifier", prefix = "gx")]
35    #[serde(
36        skip_serializing_if = "Vec::is_empty",
37        default,
38        with = "crate::serde_vec_identifier_to_map"
39    )]
40    pub identifiers: Vec<Identifier>,
41
42    /// The name(s) of the person or organization. If more than one name is
43    /// provided, names are assumed to be given in order of preference, with
44    /// the most preferred name in the first position in the list.
45    #[yaserde(rename = "name", prefix = "gx")]
46    #[serde(skip_serializing_if = "Vec::is_empty", default)]
47    pub names: Vec<TextValue>,
48
49    /// The homepage of the person or organization. Note this is different from
50    /// the homepage of the service where the person or organization has an
51    /// account.
52    #[yaserde(prefix = "gx")]
53    pub homepage: Option<ResourceReference>,
54
55    /// The [openid](https://openid.net) of the person or organization.
56    #[yaserde(prefix = "gx")]
57    pub openid: Option<ResourceReference>,
58
59    /// The online account(s) of the person or organization.
60    #[yaserde(rename = "account", prefix = "gx")]
61    #[serde(skip_serializing_if = "Vec::is_empty", default)]
62    pub accounts: Vec<OnlineAccount>,
63
64    /// The email address(es) of the person or organization. If provided, MUST
65    /// resolve to a valid e-mail address (e.g. "mailto:someone@gedcomx.org").
66    #[yaserde(rename = "email", prefix = "gx")]
67    #[serde(skip_serializing_if = "Vec::is_empty", default)]
68    pub emails: Vec<ResourceReference>,
69
70    /// The phone(s) (voice, fax, mobile) of the person or organization. If
71    /// provided, MUST resolve to a valid phone number (e.g.
72    /// "tel:+1-201-555-0123").
73    #[yaserde(rename = "phone", prefix = "gx")]
74    #[serde(skip_serializing_if = "Vec::is_empty", default)]
75    pub phones: Vec<ResourceReference>,
76
77    /// The address(es) of the person or organization.
78    #[yaserde(rename = "address", prefix = "gx")]
79    #[serde(skip_serializing_if = "Vec::is_empty", default)]
80    pub addresses: Vec<Address>,
81
82    /// A reference to the person that describes this agent. MUST resolve to an
83    /// instance of [Person](crate::Person).
84    #[yaserde(prefix = "gx")]
85    pub person: Option<ResourceReference>,
86}
87
88impl Agent {
89    #[allow(clippy::too_many_arguments)]
90    pub fn new(
91        id: Option<Id>,
92        identifiers: Vec<Identifier>,
93        names: Vec<TextValue>,
94        homepage: Option<ResourceReference>,
95        openid: Option<ResourceReference>,
96        accounts: Vec<OnlineAccount>,
97        emails: Vec<ResourceReference>,
98        phones: Vec<ResourceReference>,
99        addresses: Vec<Address>,
100        person: Option<ResourceReference>,
101    ) -> Self {
102        Self {
103            id,
104            identifiers,
105            names,
106            homepage,
107            openid,
108            accounts,
109            emails,
110            phones,
111            addresses,
112            person,
113        }
114    }
115
116    pub fn builder() -> AgentBuilder {
117        AgentBuilder::new()
118    }
119}
120
121impl Arbitrary for Agent {
122    fn arbitrary(g: &mut Gen) -> Self {
123        let mut agent = Self::builder()
124            .id(Id::arbitrary(g))
125            .identifier(Identifier::arbitrary(g))
126            .name(TextValue::arbitrary(g))
127            .homepage(ResourceReference::arbitrary(g))
128            .openid(ResourceReference::arbitrary(g))
129            .account(OnlineAccount::arbitrary(g))
130            .email(ResourceReference::arbitrary(g))
131            .phone(ResourceReference::arbitrary(g))
132            .address(Address::arbitrary(g))
133            .build();
134
135        agent.person = Some(ResourceReference::arbitrary(g));
136
137        agent
138    }
139}
140
141pub struct AgentBuilder(Agent);
142
143impl AgentBuilder {
144    pub(crate) fn new() -> Self {
145        Self(Agent::default())
146    }
147
148    pub fn id<I: Into<Id>>(&mut self, id: I) -> &mut Self {
149        self.0.id = Some(id.into());
150        self
151    }
152
153    pub fn identifier(&mut self, identifier: Identifier) -> &mut Self {
154        self.0.identifiers.push(identifier);
155        self
156    }
157
158    pub fn name<I: Into<TextValue>>(&mut self, name: I) -> &mut Self {
159        self.0.names.push(name.into());
160        self
161    }
162
163    pub fn homepage<I: Into<ResourceReference>>(&mut self, homepage: I) -> &mut Self {
164        self.0.homepage = Some(homepage.into());
165        self
166    }
167
168    pub fn openid<I: Into<ResourceReference>>(&mut self, openid: I) -> &mut Self {
169        self.0.openid = Some(openid.into());
170        self
171    }
172
173    pub fn account(&mut self, account: OnlineAccount) -> &mut Self {
174        self.0.accounts.push(account);
175        self
176    }
177
178    pub fn email<I: Into<ResourceReference>>(&mut self, email: I) -> &mut Self {
179        self.0.emails.push(email.into());
180        self
181    }
182
183    pub fn phone<I: Into<ResourceReference>>(&mut self, phone: I) -> &mut Self {
184        self.0.phones.push(phone.into());
185        self
186    }
187
188    pub fn address(&mut self, address: Address) -> &mut Self {
189        self.0.addresses.push(address);
190        self
191    }
192
193    /// # Errors
194    ///
195    /// Will return [`GedcomxError::NoId`](crate::GedcomxError::NoId) if a
196    /// conversion into [`ResourceReference`](crate::ResourceReference) fails.
197    /// This happens if `person` has no `id` set.
198    pub fn person(&mut self, person: &Person) -> Result<&mut Self> {
199        self.0.person = Some(person.try_into()?);
200        Ok(self)
201    }
202
203    pub fn build(&self) -> Agent {
204        Agent::new(
205            self.0.id.clone(),
206            self.0.identifiers.clone(),
207            self.0.names.clone(),
208            self.0.homepage.clone(),
209            self.0.openid.clone(),
210            self.0.accounts.clone(),
211            self.0.emails.clone(),
212            self.0.phones.clone(),
213            self.0.addresses.clone(),
214            self.0.person.clone(),
215        )
216    }
217}
218
219#[cfg(test)]
220mod test {
221    use pretty_assertions::assert_eq;
222    use yaserde::ser::Config;
223
224    use super::*;
225    use crate::{GedcomxError, IdentifierType};
226
227    #[test]
228    fn builder() {
229        let identifier = Identifier::new("primaryIdentifier", Some(IdentifierType::Primary));
230
231        let account = OnlineAccount::new("http://familysearch.org/", "Family Search Account");
232
233        let person = Person::builder().id("P-1").build();
234
235        let agent_1 = Agent {
236            id: Some("local_id".into()),
237            identifiers: vec![identifier.clone()],
238            names: vec![
239                "Ephraim Kunz".into(),
240                TextValue::new("Ephraim Kunz Spanish", Some("es")),
241            ],
242            homepage: Some("www.ephraimkunz.com".into()),
243            openid: Some("some_openid_value".into()),
244            accounts: vec![account.clone()],
245            emails: vec![
246                "mailto:someone@gedcomx.org".into(),
247                "mailto:someone2@gedcomx.org".into(),
248            ],
249            phones: vec!["tel:+1-201-555-0123".into()],
250            addresses: vec![Address::builder().country("United States").build()],
251            person: Some((&person).try_into().unwrap()),
252        };
253
254        let agent_2 = Agent::builder()
255            .id("local_id")
256            .identifier(identifier)
257            .name("Ephraim Kunz")
258            .name(TextValue::new("Ephraim Kunz Spanish", Some("es")))
259            .homepage("www.ephraimkunz.com")
260            .openid("some_openid_value")
261            .account(account)
262            .email("mailto:someone@gedcomx.org")
263            .email("mailto:someone2@gedcomx.org")
264            .phone("tel:+1-201-555-0123")
265            .address(Address::builder().country("United States").build())
266            .person(&person)
267            .unwrap()
268            .build();
269
270        assert_eq!(agent_1, agent_2);
271    }
272
273    #[test]
274    fn builder_fails_correctly() {
275        let person = Person::builder().build();
276        let agent = Agent::builder().person(&person).map(|b| b.build());
277        assert_eq!(
278            agent.unwrap_err().to_string(),
279            GedcomxError::no_id_error(&person).to_string()
280        );
281    }
282
283    #[test]
284    fn json_deserialize() {
285        let json = r##"{
286            "id" : "local_id",
287            "names" : [ ],
288            "homepage" : {
289              "resource" : "..."
290            },
291            "openid" : {
292              "resource" : "..."
293            },
294            "accounts" : [ ],
295            "emails" : [ { "resource" : "mailto:someone@gedcomx.org" } , { "resource" : "mailto:someone@somewhere-else.org" } ],
296            "phones" : [ { "resource" : "tel:+1-201-555-0123" } , { "resource" : "fax:+1-201-555-5555" } ],
297            "addresses" : [ ]
298          }"##;
299
300        let agent = Agent::builder()
301            .id("local_id")
302            .homepage("...")
303            .openid("...")
304            .email("mailto:someone@gedcomx.org")
305            .email("mailto:someone@somewhere-else.org")
306            .phone("tel:+1-201-555-0123")
307            .phone("fax:+1-201-555-5555")
308            .build();
309
310        assert_eq!(serde_json::from_str::<Agent>(json).unwrap(), agent);
311    }
312
313    #[test]
314    fn json_serialize() {
315        let expected = r##"{"id":"local_id","homepage":{"resource":"..."},"openid":{"resource":"..."},"emails":[{"resource":"mailto:someone@gedcomx.org"},{"resource":"mailto:someone@somewhere-else.org"}],"phones":[{"resource":"tel:+1-201-555-0123"},{"resource":"fax:+1-201-555-5555"}]}"##;
316
317        let agent = Agent::builder()
318            .id("local_id")
319            .homepage("...")
320            .openid("...")
321            .email("mailto:someone@gedcomx.org")
322            .email("mailto:someone@somewhere-else.org")
323            .phone("tel:+1-201-555-0123")
324            .phone("fax:+1-201-555-5555")
325            .build();
326
327        assert_eq!(serde_json::to_string(&agent).unwrap(), expected);
328    }
329
330    #[test]
331    fn xml_deserialize() {
332        let xml = r##"
333        <agent xmlns="http://gedcomx.org/v1/" id="local_id">
334            <identifier type="http://gedcomx.org/Primary">primaryIdentifier</identifier>
335            <name>Ephraim Kunz</name>
336            <name lang="es">Ephraim Kunz Spanish</name>
337            <homepage resource="www.ephraimkunz.com"/>
338            <openid resource="some_openid_value"/>
339            <account>
340                <serviceHomepage resource="http://familysearch.org/"/>
341                <accountName>Family Search Account</accountName>
342            </account>
343            <email resource="mailto:someone@gedcomx.org"/>
344            <email resource="mailto:someone2@gedcomx.org"/>
345            <phone resource="tel:+1-201-555-0123"/>
346            <address>
347                <country>United States</country>
348            </address>
349            <person resource="#P-1"/>    
350        </agent>"##;
351
352        let person = Person::builder().id("P-1").build();
353
354        let expected_agent = Agent::builder()
355            .id("local_id")
356            .identifier(Identifier::new(
357                "primaryIdentifier",
358                Some(IdentifierType::Primary),
359            ))
360            .name("Ephraim Kunz")
361            .name(TextValue::new("Ephraim Kunz Spanish", Some("es")))
362            .homepage("www.ephraimkunz.com")
363            .openid("some_openid_value")
364            .account(OnlineAccount::new(
365                "http://familysearch.org/",
366                "Family Search Account",
367            ))
368            .email("mailto:someone@gedcomx.org")
369            .email("mailto:someone2@gedcomx.org")
370            .phone("tel:+1-201-555-0123")
371            .address(Address::builder().country("United States").build())
372            .person(&person)
373            .unwrap()
374            .build();
375        let agent: Agent = yaserde::de::from_str(xml).unwrap();
376
377        assert_eq!(agent, expected_agent);
378    }
379
380    #[test]
381    fn xml_serialize() {
382        let person = Person::builder().id("P-1").build();
383        let agent = Agent::builder()
384            .id("local_id")
385            .identifier(Identifier::new(
386                "primaryIdentifier",
387                Some(IdentifierType::Primary),
388            ))
389            .name("Ephraim Kunz")
390            .name(TextValue::new("Ephraim Kunz Spanish", Some("es")))
391            .homepage("www.ephraimkunz.com")
392            .openid("some_openid_value")
393            .account(OnlineAccount::new(
394                "http://familysearch.org/",
395                "Family Search Account",
396            ))
397            .email("mailto:someone@gedcomx.org")
398            .email("mailto:someone2@gedcomx.org")
399            .phone("tel:+1-201-555-0123")
400            .address(Address::builder().country("United States").build())
401            .person(&person)
402            .unwrap()
403            .build();
404
405        let config = Config {
406            write_document_declaration: false,
407            ..Config::default()
408        };
409        let xml = yaserde::ser::to_string_with_config(&agent, &config).unwrap();
410        let expected_xml = r##"<agent xmlns="http://gedcomx.org/v1/" id="local_id"><identifier type="http://gedcomx.org/Primary">primaryIdentifier</identifier><name>Ephraim Kunz</name><name xml:lang="es">Ephraim Kunz Spanish</name><homepage resource="www.ephraimkunz.com" /><openid resource="some_openid_value" /><account><serviceHomepage resource="http://familysearch.org/" /><accountName>Family Search Account</accountName></account><email resource="mailto:someone@gedcomx.org" /><email resource="mailto:someone2@gedcomx.org" /><phone resource="tel:+1-201-555-0123" /><address><country>United States</country></address><person resource="#P-1" /></agent>"##;
411        assert_eq!(xml, expected_xml);
412    }
413
414    #[quickcheck_macros::quickcheck]
415    fn roundtrip_json(input: Agent) -> bool {
416        let json = serde_json::to_string(&input).unwrap();
417        let from_json: Agent = serde_json::from_str(&json).unwrap();
418        input == from_json
419    }
420
421    #[quickcheck_macros::quickcheck]
422    fn roundtrip_xml(input: Agent) -> bool {
423        let xml = yaserde::ser::to_string(&input).unwrap();
424        let from_xml: Agent = yaserde::de::from_str(&xml).unwrap();
425        input == from_xml
426    }
427}