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#[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 #[yaserde(attribute)]
31 pub id: Option<Id>,
32
33 #[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 #[yaserde(rename = "name", prefix = "gx")]
46 #[serde(skip_serializing_if = "Vec::is_empty", default)]
47 pub names: Vec<TextValue>,
48
49 #[yaserde(prefix = "gx")]
53 pub homepage: Option<ResourceReference>,
54
55 #[yaserde(prefix = "gx")]
57 pub openid: Option<ResourceReference>,
58
59 #[yaserde(rename = "account", prefix = "gx")]
61 #[serde(skip_serializing_if = "Vec::is_empty", default)]
62 pub accounts: Vec<OnlineAccount>,
63
64 #[yaserde(rename = "email", prefix = "gx")]
67 #[serde(skip_serializing_if = "Vec::is_empty", default)]
68 pub emails: Vec<ResourceReference>,
69
70 #[yaserde(rename = "phone", prefix = "gx")]
74 #[serde(skip_serializing_if = "Vec::is_empty", default)]
75 pub phones: Vec<ResourceReference>,
76
77 #[yaserde(rename = "address", prefix = "gx")]
79 #[serde(skip_serializing_if = "Vec::is_empty", default)]
80 pub addresses: Vec<Address>,
81
82 #[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 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}