1use std::{borrow::Cow, str::FromStr};
4
5use thiserror::Error;
6
7use crate::{
8 EML_SCHEMA_VERSION, EMLError, EMLValueResultExt as _, NS_EML, NS_KR,
9 common::{
10 CandidateIdentifier, CanonicalizationMethod, CreationDateTime, ElectionDomain, IssueDate,
11 ListData, ManagingAuthority, PersonNameStructure, TransactionId,
12 },
13 documents::{
14 ElectionIdentifierBuilder, accepted_root, validate_category_and_subcategory,
15 validate_election_and_nomination_dates,
16 },
17 error::EMLErrorKind,
18 io::{
19 EMLElement, EMLElementReader, EMLElementWriter, EMLReadElement as _, QualifiedName,
20 collect_struct, write_eml_element,
21 },
22 utils::{
23 AffiliationType, ContestId, ElectionCategory, ElectionId, ElectionSubcategory, Gender,
24 StringValue, StringValueData, XsDate, XsDateOrDateTime, XsDateTime,
25 },
26};
27
28use super::candidate_lists::{
29 QualifyingAddress, QualifyingAddressCountry, QualifyingAddressLocality,
30};
31
32pub(crate) const EML_NOMINATION_ID: &str = "210";
34
35#[derive(Debug, Clone)]
37pub struct Nomination {
38 pub transaction_id: TransactionId,
40
41 pub managing_authority: Option<ManagingAuthority>,
43
44 pub issue_date: IssueDate,
46
47 pub creation_date_time: CreationDateTime,
49
50 pub canonicalization_method: Option<CanonicalizationMethod>,
52
53 pub nomination_data: NominationData,
55}
56
57impl Nomination {
58 pub fn builder() -> NominationBuilder {
60 NominationBuilder::new()
61 }
62}
63
64impl FromStr for Nomination {
65 type Err = EMLError;
66
67 fn from_str(s: &str) -> Result<Self, Self::Err> {
68 use crate::io::EMLRead as _;
69 Self::parse_eml(s, crate::io::EMLParsingMode::Strict).ok()
70 }
71}
72
73impl TryFrom<&str> for Nomination {
74 type Error = EMLError;
75
76 fn try_from(value: &str) -> Result<Self, Self::Error> {
77 use crate::io::EMLRead as _;
78 Self::parse_eml(value, crate::io::EMLParsingMode::Strict).ok()
79 }
80}
81
82impl TryFrom<Nomination> for String {
83 type Error = EMLError;
84
85 fn try_from(value: Nomination) -> Result<Self, Self::Error> {
86 use crate::io::EMLWrite as _;
87 value.write_eml_root_str(true, true)
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct NominationBuilder {
94 transaction_id: Option<TransactionId>,
95 managing_authority: Option<ManagingAuthority>,
96 issue_date: Option<IssueDate>,
97 creation_date_time: Option<CreationDateTime>,
98 canonicalization_method: Option<CanonicalizationMethod>,
99 nomination_data: Option<NominationData>,
100 election_identifier: Option<NominationElectionIdentifier>,
101 contest_identifier: Option<NominationContestIdentifier>,
102 affiliation: Option<NominationAffiliation>,
103 nominate: Option<NominationNominate>,
104}
105
106impl NominationBuilder {
107 pub fn new() -> Self {
109 NominationBuilder {
110 transaction_id: None,
111 managing_authority: None,
112 issue_date: None,
113 creation_date_time: None,
114 canonicalization_method: None,
115 nomination_data: None,
116 election_identifier: None,
117 contest_identifier: None,
118 affiliation: None,
119 nominate: None,
120 }
121 }
122
123 pub fn transaction_id(mut self, transaction_id: impl Into<TransactionId>) -> Self {
125 self.transaction_id = Some(transaction_id.into());
126 self
127 }
128
129 pub fn managing_authority(mut self, managing_authority: impl Into<ManagingAuthority>) -> Self {
131 self.managing_authority = Some(managing_authority.into());
132 self
133 }
134
135 pub fn issue_date(mut self, issue_date: impl Into<XsDateOrDateTime>) -> Self {
137 self.issue_date = Some(IssueDate::new(issue_date.into()));
138 self
139 }
140
141 pub fn creation_date_time(mut self, creation_date_time: impl Into<XsDateTime>) -> Self {
143 self.creation_date_time = Some(CreationDateTime::new(creation_date_time.into()));
144 self
145 }
146
147 pub fn canonicalization_method(
149 mut self,
150 canonicalization_method: impl Into<CanonicalizationMethod>,
151 ) -> Self {
152 self.canonicalization_method = Some(canonicalization_method.into());
153 self
154 }
155
156 pub fn nomination_data(mut self, nomination_data: impl Into<NominationData>) -> Self {
163 self.nomination_data = Some(nomination_data.into());
164 self
165 }
166
167 pub fn election_identifier(
172 mut self,
173 election_identifier: impl Into<NominationElectionIdentifier>,
174 ) -> Self {
175 self.election_identifier = Some(election_identifier.into());
176 self
177 }
178
179 pub fn contest_identifier(
184 mut self,
185 contest_identifier: impl Into<NominationContestIdentifier>,
186 ) -> Self {
187 self.contest_identifier = Some(contest_identifier.into());
188 self
189 }
190
191 pub fn affiliation(mut self, affiliation: impl Into<NominationAffiliation>) -> Self {
196 self.affiliation = Some(affiliation.into());
197 self
198 }
199
200 pub fn nominate(mut self, nominate: impl Into<NominationNominate>) -> Self {
205 self.nominate = Some(nominate.into());
206 self
207 }
208
209 pub fn build(self) -> Result<Nomination, EMLError> {
211 Ok(Nomination {
212 transaction_id: self
213 .transaction_id
214 .ok_or(EMLErrorKind::MissingBuildProperty("transaction_id").without_span())?,
215 managing_authority: self.managing_authority,
216 issue_date: self
217 .issue_date
218 .ok_or(EMLErrorKind::MissingBuildProperty("issue_date").without_span())?,
219 creation_date_time: self
220 .creation_date_time
221 .ok_or(EMLErrorKind::MissingBuildProperty("creation_date_time").without_span())?,
222 canonicalization_method: self.canonicalization_method,
223 nomination_data: self.nomination_data.map_or_else(
224 || {
225 Ok(NominationData {
226 election_identifier: self.election_identifier.ok_or(
227 EMLErrorKind::MissingBuildProperty("election_identifier")
228 .without_span(),
229 )?,
230 contest_identifier: self.contest_identifier.ok_or(
231 EMLErrorKind::MissingBuildProperty("contest_identifier").without_span(),
232 )?,
233 affiliation: self.affiliation.ok_or(
234 EMLErrorKind::MissingBuildProperty("affiliation").without_span(),
235 )?,
236 nominate: self
237 .nominate
238 .ok_or(EMLErrorKind::MissingBuildProperty("nominate").without_span())?,
239 })
240 },
241 Ok,
242 )?,
243 })
244 }
245}
246
247impl Default for NominationBuilder {
248 fn default() -> Self {
249 Self::new()
250 }
251}
252
253impl EMLElement for Nomination {
254 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("EML", Some(NS_EML));
255
256 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
257 accepted_root(elem)?;
258
259 let document_id = elem.attribute_value_req(("Id", None))?;
260 if document_id.as_ref() != EML_NOMINATION_ID {
261 return Err(
262 EMLErrorKind::InvalidDocumentType("210", document_id.to_string())
263 .with_span(elem.span()),
264 );
265 }
266
267 Ok(collect_struct!(elem, Nomination {
268 transaction_id: TransactionId::EML_NAME => |elem| TransactionId::read_eml(elem)?,
269 managing_authority as Option: ManagingAuthority::EML_NAME => |elem| ManagingAuthority::read_eml(elem)?,
270 issue_date: IssueDate::EML_NAME => |elem| IssueDate::read_eml(elem)?,
271 creation_date_time: CreationDateTime::EML_NAME => |elem| CreationDateTime::read_eml(elem)?,
272 canonicalization_method as Option: CanonicalizationMethod::EML_NAME => |elem| CanonicalizationMethod::read_eml(elem)?,
273 nomination_data: NominationData::EML_NAME => |elem| NominationData::read_eml(elem)?,
274 }))
275 }
276
277 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
278 writer
279 .attr(("Id", None), EML_NOMINATION_ID)?
280 .attr(("SchemaVersion", None), EML_SCHEMA_VERSION)?
281 .child_elem(TransactionId::EML_NAME, &self.transaction_id)?
282 .child_elem_option(
283 ManagingAuthority::EML_NAME,
284 self.managing_authority.as_ref(),
285 )?
286 .child_elem(IssueDate::EML_NAME, &self.issue_date)?
287 .child_elem(CreationDateTime::EML_NAME, &self.creation_date_time)?
288 .child_elem(NominationData::EML_NAME, &self.nomination_data)?
294 .finish()
295 }
296}
297
298#[derive(Debug, Clone)]
300pub struct NominationData {
301 pub election_identifier: NominationElectionIdentifier,
303
304 pub contest_identifier: NominationContestIdentifier,
306
307 pub affiliation: NominationAffiliation,
309
310 pub nominate: NominationNominate,
312}
313
314impl EMLElement for NominationData {
315 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Nomination", Some(NS_EML));
316
317 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
318 Ok(collect_struct!(elem, NominationData {
319 election_identifier: NominationElectionIdentifier::EML_NAME => |elem| NominationElectionIdentifier::read_eml(elem)?,
320 contest_identifier: NominationContestIdentifier::EML_NAME => |elem| NominationContestIdentifier::read_eml(elem)?,
321 affiliation: NominationAffiliation::EML_NAME => |elem| NominationAffiliation::read_eml(elem)?,
322 nominate: NominationNominate::EML_NAME => |elem| NominationNominate::read_eml(elem)?,
323 }))
324 }
325
326 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
327 writer
328 .child_elem(
329 NominationElectionIdentifier::EML_NAME,
330 &self.election_identifier,
331 )?
332 .child_elem(
333 NominationContestIdentifier::EML_NAME,
334 &self.contest_identifier,
335 )?
336 .child_elem(NominationAffiliation::EML_NAME, &self.affiliation)?
337 .child_elem(NominationNominate::EML_NAME, &self.nominate)?
338 .finish()
339 }
340}
341
342#[derive(Debug, Clone)]
344pub struct NominationElectionIdentifier {
345 pub id: StringValue<ElectionId>,
347
348 pub name: Option<Box<str>>,
350
351 pub category: StringValue<ElectionCategory>,
353
354 pub subcategory: Option<StringValue<ElectionSubcategory>>,
356
357 pub domain: Option<ElectionDomain>,
359
360 pub election_date: StringValue<XsDate>,
362
363 pub nomination_date: StringValue<XsDate>,
365}
366
367impl NominationElectionIdentifier {
368 pub fn builder() -> ElectionIdentifierBuilder {
370 ElectionIdentifierBuilder::new()
371 }
372}
373
374impl EMLElement for NominationElectionIdentifier {
375 const EML_NAME: QualifiedName<'_, '_> =
376 QualifiedName::from_static("ElectionIdentifier", Some(NS_EML));
377
378 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
379 let data = collect_struct!(
380 elem,
381 NominationElectionIdentifier {
382 id: elem.string_value_attr("Id", None)?,
383 name as Option: ("ElectionName", NS_EML) => |elem| elem.text_without_children()?,
384 category: ("ElectionCategory", NS_EML) => |elem| elem.string_value()?,
385 subcategory as Option: ("ElectionSubcategory", NS_KR) => |elem| elem.string_value()?,
386 domain as Option: ElectionDomain::EML_NAME => |elem| ElectionDomain::read_eml(elem)?,
387 election_date: ("ElectionDate", NS_KR) => |elem| elem.string_value()?,
388 nomination_date: ("NominationDate", NS_KR) => |elem| elem.string_value()?,
389 }
390 );
391
392 if let Err(e) = validate_election_and_nomination_dates(
393 Some(&data.election_date),
394 Some(&data.nomination_date),
395 ) {
396 let e = e.into_kind().with_span(elem.full_span());
397 if elem.parsing_mode().is_strict() {
398 return Err(e);
399 } else {
400 elem.push_err(e);
401 }
402 }
403
404 if let Err(e) = validate_category_and_subcategory(&data.category, data.subcategory.as_ref())
405 {
406 let e = e.into_kind().with_span(elem.full_span());
407 if elem.parsing_mode().is_strict() {
408 return Err(e);
409 } else {
410 elem.push_err(e);
411 }
412 }
413
414 Ok(data)
415 }
416
417 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
418 writer
419 .attr("Id", self.id.raw().as_ref())?
420 .child_option(
421 ("ElectionName", NS_EML),
422 self.name.as_ref(),
423 |elem, value| elem.text(value.as_ref())?.finish(),
424 )?
425 .child(("ElectionCategory", NS_EML), |elem| {
426 elem.text(self.category.raw().as_ref())?.finish()
427 })?
428 .child_option(
429 ("ElectionSubcategory", NS_KR),
430 self.subcategory.as_ref(),
431 |elem, value| elem.text(value.raw().as_ref())?.finish(),
432 )?
433 .child_elem_option(ElectionDomain::EML_NAME, self.domain.as_ref())?
434 .child(("ElectionDate", NS_KR), |elem| {
435 elem.text(self.election_date.raw().as_ref())?.finish()
436 })?
437 .child(("NominationDate", NS_KR), |elem| {
438 elem.text(self.nomination_date.raw().as_ref())?.finish()
439 })?
440 .finish()
441 }
442}
443
444#[derive(Debug, Clone)]
446pub struct NominationContestIdentifier {
447 pub id: StringValue<ContestId>,
449
450 pub name: Box<str>,
452}
453
454impl NominationContestIdentifier {
455 pub fn new(id: ContestId, name: impl Into<Box<str>>) -> Self {
457 NominationContestIdentifier {
458 id: StringValue::Parsed(id),
459 name: name.into(),
460 }
461 }
462}
463
464impl EMLElement for NominationContestIdentifier {
465 const EML_NAME: QualifiedName<'_, '_> =
466 QualifiedName::from_static("ContestIdentifier", Some(NS_EML));
467
468 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
469 Ok(collect_struct!(
470 elem,
471 NominationContestIdentifier {
472 id: elem.string_value_attr("Id", None)?,
473 name: ("ContestName", NS_EML) => |elem| elem.text_without_children()?,
474 }
475 ))
476 }
477
478 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
479 writer
480 .attr("Id", self.id.raw().as_ref())?
481 .child(("ContestName", NS_EML), |elem| {
482 elem.text(self.name.as_ref())?.finish()
483 })?
484 .finish()
485 }
486}
487
488#[derive(Debug, Clone)]
493pub struct NominationAffiliation {
494 pub registered_name: Box<str>,
496
497 pub affiliation_type: StringValue<AffiliationType>,
499
500 pub list_data: ListData,
502
503 pub candidates: Vec<NominationCandidate>,
505}
506
507impl EMLElement for NominationAffiliation {
508 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Affiliation", Some(NS_EML));
509
510 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
511 struct NominationAffiliationIdentifier {
513 registered_name: Box<str>,
514 }
515
516 impl EMLElement for NominationAffiliationIdentifier {
517 const EML_NAME: QualifiedName<'_, '_> =
518 QualifiedName::from_static("AffiliationIdentifier", Some(NS_EML));
519
520 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
521 Ok(collect_struct!(
522 elem,
523 NominationAffiliationIdentifier {
524 registered_name: ("RegisteredName", NS_EML) => |elem| elem.text_without_children()?,
525 }
526 ))
527 }
528
529 fn write_eml(&self, _writer: EMLElementWriter) -> Result<(), EMLError> {
530 unreachable!()
531 }
532 }
533
534 let data = collect_struct!(elem, NominationAffiliation {
535 registered_name: NominationAffiliationIdentifier::EML_NAME => |elem| {
536 let id = NominationAffiliationIdentifier::read_eml(elem)?;
537 id.registered_name
538 },
539 affiliation_type: ("Type", NS_EML) => |elem| elem.string_value()?,
540 list_data: ListData::EML_NAME => |elem| ListData::read_eml(elem)?,
541 candidates as Vec: NominationCandidate::EML_NAME => |elem| NominationCandidate::read_eml(elem)?,
542 });
543
544 if data.candidates.is_empty() {
545 let err = EMLErrorKind::MissingElement(NominationCandidate::EML_NAME.as_owned())
546 .with_span(elem.full_span());
547 if elem.parsing_mode().is_strict() {
548 return Err(err);
549 } else {
550 elem.push_err(err);
551 }
552 }
553
554 Ok(data)
555 }
556
557 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
558 writer
559 .child(("AffiliationIdentifier", NS_EML), |w| {
560 w.child(("RegisteredName", NS_EML), |w| {
561 w.text(self.registered_name.as_ref())?.finish()
562 })?
563 .finish()
564 })?
565 .child(("Type", NS_EML), |elem| {
566 elem.text(self.affiliation_type.raw().as_ref())?.finish()
567 })?
568 .child_elem(ListData::EML_NAME, &self.list_data)?
569 .child_elems(NominationCandidate::EML_NAME, &self.candidates)?
570 .finish()
571 }
572}
573
574#[derive(Debug, Clone)]
580pub struct NominationCandidate {
581 pub identifier: CandidateIdentifier,
583
584 pub full_name: PersonNameStructure,
586
587 pub date_of_birth: Option<StringValue<XsDate>>,
589
590 pub gender: StringValue<Gender>,
592
593 pub qualifying_address: QualifyingAddress,
595
596 pub contact: Option<NominationContact>,
598
599 pub agent: Option<NominationAgent>,
601
602 pub date_of_birth_annex: Option<Box<str>>,
604
605 pub national_identification_number: Option<Box<str>>,
607}
608
609impl EMLElement for NominationCandidate {
610 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Candidate", Some(NS_EML));
611
612 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
613 Ok(collect_struct!(elem, NominationCandidate {
614 identifier: CandidateIdentifier::EML_NAME => |elem| CandidateIdentifier::read_eml(elem)?,
615 full_name: ("CandidateFullName", NS_EML) => |elem| PersonNameStructure::read_eml_element(elem)?,
616 date_of_birth as Option: ("DateOfBirth", NS_EML) => |elem| elem.string_value()?,
617 gender: ("Gender", NS_EML) => |elem| elem.string_value()?,
618 qualifying_address: QualifyingAddress::EML_NAME => |elem| QualifyingAddress::read_eml(elem)?,
619 contact as Option: NominationContact::EML_NAME => |elem| NominationContact::read_eml(elem)?,
620 agent as Option: NominationAgent::EML_NAME => |elem| NominationAgent::read_eml(elem)?,
621 date_of_birth_annex as Option: ("DateOfBirthAnnex", NS_KR) => |elem| elem.text_without_children()?,
622 national_identification_number as Option: ("NationalIdentificationNumber", NS_KR) => |elem| elem.text_without_children()?,
623 }))
624 }
625
626 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
627 writer
628 .child_elem(CandidateIdentifier::EML_NAME, &self.identifier)?
629 .child(
630 ("CandidateFullName", NS_EML),
631 write_eml_element(&self.full_name),
632 )?
633 .child_option(
634 ("DateOfBirth", NS_EML),
635 self.date_of_birth.as_ref(),
636 |elem, value| elem.text(value.raw().as_ref())?.finish(),
637 )?
638 .child(("Gender", NS_EML), |elem| {
639 elem.text(self.gender.raw().as_ref())?.finish()
640 })?
641 .child_elem(QualifyingAddress::EML_NAME, &self.qualifying_address)?
642 .child_elem_option(NominationContact::EML_NAME, self.contact.as_ref())?
643 .child_elem_option(NominationAgent::EML_NAME, self.agent.as_ref())?
644 .child_option(
645 ("DateOfBirthAnnex", NS_KR),
646 self.date_of_birth_annex.as_ref(),
647 |elem, value| elem.text(value.as_ref())?.finish(),
648 )?
649 .child_option(
650 ("NationalIdentificationNumber", NS_KR),
651 self.national_identification_number.as_ref(),
652 |elem, value| elem.text(value.as_ref())?.finish(),
653 )?
654 .finish()
655 }
656}
657
658#[derive(Debug, Clone)]
660pub struct NominationContact {
661 pub mailing_address: MailingAddress,
663}
664
665impl EMLElement for NominationContact {
666 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Contact", Some(NS_EML));
667
668 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
669 Ok(collect_struct!(elem, NominationContact {
670 mailing_address: MailingAddress::EML_NAME => |elem| MailingAddress::read_eml(elem)?,
671 }))
672 }
673
674 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
675 writer
676 .child_elem(MailingAddress::EML_NAME, &self.mailing_address)?
677 .finish()
678 }
679}
680
681#[derive(Debug, Clone)]
683pub struct MailingAddress {
684 pub address: QualifyingAddress,
686}
687
688impl MailingAddress {
689 pub fn new(address: impl Into<QualifyingAddress>) -> Self {
691 MailingAddress {
692 address: address.into(),
693 }
694 }
695}
696
697impl EMLElement for MailingAddress {
698 const EML_NAME: QualifiedName<'_, '_> =
699 QualifiedName::from_static("MailingAddress", Some(NS_EML));
700
701 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
702 let parent_name = elem.name()?.as_owned();
703 let mut found_value = None;
704 while let Some(mut next_child) = elem.next_child()? {
705 let name = next_child.name()?;
706 if found_value.is_some()
707 || name != QualifyingAddressLocality::EML_NAME
708 && name != QualifyingAddressCountry::EML_NAME
709 {
710 let err = EMLErrorKind::UnexpectedElement(name.as_owned(), parent_name.clone())
711 .with_span(next_child.span());
712 if next_child.parsing_mode().is_strict() {
713 return Err(err);
714 } else {
715 next_child.push_err(err);
716 next_child.skip()?;
717 }
718 } else {
719 match name {
720 name if name == QualifyingAddressLocality::EML_NAME => {
721 let locality = QualifyingAddressLocality::read_eml(&mut next_child)?;
722 found_value = Some(QualifyingAddress::Locality(locality));
723 }
724 name if name == QualifyingAddressCountry::EML_NAME => {
725 let country = QualifyingAddressCountry::read_eml(&mut next_child)?;
726 found_value = Some(QualifyingAddress::Country(country));
727 }
728 _ => unreachable!(),
729 }
730 }
731 }
732 let Some(value) = found_value else {
733 return Err(EMLErrorKind::MissingChoiceElements(vec![
734 QualifyingAddressLocality::EML_NAME.as_owned(),
735 QualifyingAddressCountry::EML_NAME.as_owned(),
736 ])
737 .with_span(elem.span()));
738 };
739 Ok(MailingAddress { address: value })
740 }
741
742 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
743 match &self.address {
744 QualifyingAddress::Locality(locality) => {
745 writer.child_elem(QualifyingAddressLocality::EML_NAME, locality)?
746 }
747 QualifyingAddress::Country(country) => {
748 writer.child_elem(QualifyingAddressCountry::EML_NAME, country)?
749 }
750 }
751 .finish()
752 }
753}
754
755#[derive(Debug, Clone)]
757pub struct NominationAgent {
758 pub role: Option<String>,
760
761 pub agent_identifier: AgentIdentifier,
763
764 pub contact: Option<NominationContact>,
766
767 pub living_address: LivingAddress,
769}
770
771impl EMLElement for NominationAgent {
772 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Agent", Some(NS_EML));
773
774 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
775 Ok(collect_struct!(elem, NominationAgent {
776 role: elem.attribute_value("Role")?.map(Cow::into_owned),
777 agent_identifier: AgentIdentifier::EML_NAME => |elem| AgentIdentifier::read_eml(elem)?,
778 contact as Option: NominationContact::EML_NAME => |elem| NominationContact::read_eml(elem)?,
779 living_address: LivingAddress::EML_NAME => |elem| LivingAddress::read_eml(elem)?,
780 }))
781 }
782
783 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
784 writer
785 .attr_opt("Role", self.role.as_ref())?
786 .child_elem(AgentIdentifier::EML_NAME, &self.agent_identifier)?
787 .child_elem_option(NominationContact::EML_NAME, self.contact.as_ref())?
788 .child_elem(LivingAddress::EML_NAME, &self.living_address)?
789 .finish()
790 }
791}
792
793#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
795pub enum NominationJobTitle {
796 Submitter,
798 DeputySubmitter,
800 CombinationRepresentative,
802 DeputyCombinationRepresentative,
804}
805
806impl NominationJobTitle {
807 pub fn new(s: impl AsRef<str>) -> Result<Self, EMLError> {
809 Self::from_eml_value(s).wrap_value_error()
810 }
811
812 pub fn from_eml_value(s: impl AsRef<str>) -> Result<Self, UnknownNominationJobTitleError> {
814 let data = s.as_ref();
815 match data {
816 "inleveraar" => Ok(NominationJobTitle::Submitter),
817 "plaatsvervanger van de inleveraar" => Ok(NominationJobTitle::DeputySubmitter),
818 "gemachtigde voor het aangaan van lijstencombinaties" => {
819 Ok(NominationJobTitle::CombinationRepresentative)
820 }
821 "plaatsvervanger voor het aangaan van lijstencombinaties" => {
822 Ok(NominationJobTitle::DeputyCombinationRepresentative)
823 }
824 _ => Err(UnknownNominationJobTitleError(data.to_string())),
825 }
826 }
827
828 pub fn to_eml_value(&self) -> &'static str {
830 match self {
831 NominationJobTitle::Submitter => "inleveraar",
832 NominationJobTitle::DeputySubmitter => "plaatsvervanger van de inleveraar",
833 NominationJobTitle::CombinationRepresentative => {
834 "gemachtigde voor het aangaan van lijstencombinaties"
835 }
836 NominationJobTitle::DeputyCombinationRepresentative => {
837 "plaatsvervanger voor het aangaan van lijstencombinaties"
838 }
839 }
840 }
841}
842
843#[derive(Debug, Clone, Error, PartialEq, Eq)]
845#[error("Unknown nomination job title: {0}")]
846pub struct UnknownNominationJobTitleError(String);
847
848impl StringValueData for NominationJobTitle {
849 type Error = UnknownNominationJobTitleError;
850
851 fn parse_from_str(s: &str) -> Result<Self, Self::Error>
852 where
853 Self: Sized,
854 {
855 Self::from_eml_value(s)
856 }
857
858 fn to_raw_value(&self) -> Box<str> {
859 self.to_eml_value().into()
860 }
861}
862
863#[derive(Debug, Clone)]
865pub struct AgentIdentifier {
866 pub agent_name: PersonNameStructure,
868}
869
870impl AgentIdentifier {
871 pub fn new(agent_name: impl Into<PersonNameStructure>) -> Self {
873 AgentIdentifier {
874 agent_name: agent_name.into(),
875 }
876 }
877}
878
879impl EMLElement for AgentIdentifier {
880 const EML_NAME: QualifiedName<'_, '_> =
881 QualifiedName::from_static("AgentIdentifier", Some(NS_EML));
882
883 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
884 Ok(collect_struct!(elem, AgentIdentifier {
885 agent_name: ("AgentName", NS_EML) => |elem| PersonNameStructure::read_eml_element(elem)?,
886 }))
887 }
888
889 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
890 writer
891 .child(("AgentName", NS_EML), write_eml_element(&self.agent_name))?
892 .finish()
893 }
894}
895
896#[derive(Debug, Clone)]
898pub struct LivingAddress {
899 pub locality_name: Box<str>,
901
902 pub country_name_code: Option<Box<str>>,
904}
905
906impl LivingAddress {
907 pub fn new(locality_name: impl Into<Box<str>>) -> Self {
909 LivingAddress {
910 locality_name: locality_name.into(),
911 country_name_code: None,
912 }
913 }
914
915 pub fn with_country_name_code(mut self, code: impl Into<Box<str>>) -> Self {
917 self.country_name_code = Some(code.into());
918 self
919 }
920}
921
922impl EMLElement for LivingAddress {
923 const EML_NAME: QualifiedName<'_, '_> =
924 QualifiedName::from_static("LivingAddress", Some(NS_KR));
925
926 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
927 Ok(collect_struct!(elem, LivingAddress {
928 locality_name: ("LocalityName", NS_KR) => |elem| elem.text_without_children()?,
929 country_name_code as Option: ("CountryNameCode", NS_KR) => |elem| elem.text_without_children()?,
930 }))
931 }
932
933 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
934 writer
935 .child(("LocalityName", NS_KR), |elem| {
936 elem.text(self.locality_name.as_ref())?.finish()
937 })?
938 .child_option(
939 ("CountryNameCode", NS_KR),
940 self.country_name_code.as_ref(),
941 |elem, value| elem.text(value.as_ref())?.finish(),
942 )?
943 .finish()
944 }
945}
946
947#[derive(Debug, Clone)]
949pub struct NominationNominate {
950 pub proposers: Vec<NominationProposer>,
952}
953
954impl NominationNominate {
955 pub fn new(proposers: Vec<NominationProposer>) -> Self {
957 NominationNominate { proposers }
958 }
959}
960
961impl EMLElement for NominationNominate {
962 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Nominate", Some(NS_EML));
963
964 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
965 let data = collect_struct!(elem, NominationNominate {
966 proposers as Vec: NominationProposer::EML_NAME => |elem| NominationProposer::read_eml(elem)?,
967 });
968
969 if data.proposers.len() < 2 {
970 let err = EMLErrorKind::MissingElement(NominationProposer::EML_NAME.as_owned())
971 .with_span(elem.full_span());
972 if elem.parsing_mode().is_strict() {
973 return Err(err);
974 } else {
975 elem.push_err(err);
976 }
977 }
978
979 Ok(data)
980 }
981
982 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
983 writer
984 .child_elems(NominationProposer::EML_NAME, &self.proposers)?
985 .finish()
986 }
987}
988
989#[derive(Debug, Clone)]
991pub struct NominationProposer {
992 pub name: PersonNameStructure,
994
995 pub contact: NominationContact,
997
998 pub job_title: StringValue<NominationJobTitle>,
1004
1005 pub id: Option<Box<str>>,
1007
1008 pub living_address: Option<LivingAddress>,
1010}
1011
1012impl EMLElement for NominationProposer {
1013 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Proposer", Some(NS_EML));
1014
1015 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
1016 Ok(collect_struct!(elem, NominationProposer {
1017 name: ("Name", NS_EML) => |elem| PersonNameStructure::read_eml_element(elem)?,
1018 contact: NominationContact::EML_NAME => |elem| NominationContact::read_eml(elem)?,
1019 job_title: ("JobTitle", NS_EML) => |elem| elem.string_value()?,
1020 id as Option: ("Id", NS_EML) => |elem| elem.text_without_children()?,
1021 living_address as Option: LivingAddress::EML_NAME => |elem| LivingAddress::read_eml(elem)?,
1022 }))
1023 }
1024
1025 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
1026 writer
1027 .child(("Name", NS_EML), write_eml_element(&self.name))?
1028 .child_elem(NominationContact::EML_NAME, &self.contact)?
1029 .child(("JobTitle", NS_EML), |elem| {
1030 elem.text(self.job_title.raw().as_ref())?.finish()
1031 })?
1032 .child_option(("Id", NS_EML), self.id.as_ref(), |elem, value| {
1033 elem.text(value.as_ref())?.finish()
1034 })?
1035 .child_elem_option(LivingAddress::EML_NAME, self.living_address.as_ref())?
1036 .finish()
1037 }
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use std::num::NonZeroU64;
1043
1044 use chrono::{NaiveDate, NaiveDateTime};
1045
1046 use super::*;
1047 use crate::{
1048 common::{AuthorityIdentifier, CandidateIdentifier, ElectionDomain, ListData, PersonName},
1049 io::{EMLParsingMode, EMLRead as _, EMLWrite as _},
1050 utils::{
1051 AffiliationType, AuthorityId, CandidateId, ContestId, ElectionCategory,
1052 ElectionDomainId, ElectionId, ElectionSubcategory, Gender, StringValue, XsDate,
1053 XsDateTime,
1054 },
1055 };
1056
1057 #[test]
1058 fn nomination_construction() {
1059 let nomination = Nomination::builder()
1060 .transaction_id(TransactionId::new(1))
1061 .managing_authority(ManagingAuthority::new(
1062 AuthorityIdentifier::new(AuthorityId::new("0000").unwrap()).with_name("Test"),
1063 ))
1064 .issue_date(XsDate::from_date(2024, 6, 10).unwrap())
1065 .creation_date_time(XsDateTime::new_without_tz(NaiveDateTime::new(
1066 NaiveDate::from_ymd_opt(2024, 6, 10).unwrap(),
1067 chrono::NaiveTime::from_hms_milli_opt(12, 0, 0, 0).unwrap(),
1068 )))
1069 .election_identifier(
1070 NominationElectionIdentifier::builder()
1071 .id(ElectionId::new("GR2026_Test").unwrap())
1072 .category(ElectionCategory::GR)
1073 .subcategory(ElectionSubcategory::GR2)
1074 .domain(ElectionDomain::new(
1075 Some(ElectionDomainId::new("0000").unwrap()),
1076 "Test",
1077 ))
1078 .election_date(XsDate::from_date(2026, 3, 18).unwrap())
1079 .nomination_date(XsDate::from_date(2026, 2, 2).unwrap())
1080 .build_for_nomination()
1081 .unwrap(),
1082 )
1083 .contest_identifier(NominationContestIdentifier::new(
1084 ContestId::new("geen").unwrap(),
1085 "Test Contest",
1086 ))
1087 .affiliation(NominationAffiliation {
1088 registered_name: "Test Party".into(),
1089 affiliation_type: StringValue::from_value(AffiliationType::StandAloneList),
1090 list_data: ListData::new(true),
1091 candidates: vec![
1092 NominationCandidate {
1093 identifier: CandidateIdentifier::new(CandidateId::new(
1094 NonZeroU64::new(1).unwrap(),
1095 )),
1096 full_name: PersonName::new("Tansen")
1097 .with_initials("J.")
1098 .with_first_name("Jan")
1099 .with_name_prefix("van")
1100 .into(),
1101 date_of_birth: Some(StringValue::from_value(
1102 XsDate::from_date(1980, 1, 15).unwrap(),
1103 )),
1104 gender: StringValue::from_value(Gender::Male),
1105 qualifying_address: QualifyingAddress::Locality(
1106 QualifyingAddressLocality::new("Amsterdam"),
1107 ),
1108 contact: None,
1109 agent: None,
1110 date_of_birth_annex: None,
1111 national_identification_number: None,
1112 },
1113 NominationCandidate {
1114 identifier: CandidateIdentifier::new(CandidateId::new(
1115 NonZeroU64::new(2).unwrap(),
1116 )),
1117 full_name: PersonName::new("Bakker")
1118 .with_initials("A.B.")
1119 .with_first_name("Anna")
1120 .into(),
1121 date_of_birth: Some(StringValue::from_value(
1122 XsDate::from_date(1990, 7, 22).unwrap(),
1123 )),
1124 gender: StringValue::from_value(Gender::Female),
1125 qualifying_address: QualifyingAddress::Country(
1126 QualifyingAddressCountry::new(Some("NL"), "Rotterdam"),
1127 ),
1128 contact: Some(NominationContact {
1129 mailing_address: MailingAddress::new(QualifyingAddress::Locality(
1130 QualifyingAddressLocality::new("Rotterdam"),
1131 )),
1132 }),
1133 agent: Some(NominationAgent {
1134 role: Some("H10".to_string()),
1135 agent_identifier: AgentIdentifier::new(
1136 PersonName::new("Groot")
1137 .with_initials("P.")
1138 .with_first_name("Pieter"),
1139 ),
1140 contact: None,
1141 living_address: LivingAddress::new("Den Haag"),
1142 }),
1143 date_of_birth_annex: Some("XX-07-1990".into()),
1144 national_identification_number: Some("123456789".into()),
1145 },
1146 ],
1147 })
1148 .nominate(NominationNominate::new(vec![
1149 NominationProposer {
1150 name: PersonName::new("Janssen")
1151 .with_initials("K.")
1152 .with_first_name("Karel")
1153 .into(),
1154 contact: NominationContact {
1155 mailing_address: MailingAddress::new(QualifyingAddress::Locality(
1156 QualifyingAddressLocality::new("Amsterdam"),
1157 )),
1158 },
1159 job_title: StringValue::from_value(NominationJobTitle::Submitter),
1160 id: None,
1161 living_address: None,
1162 },
1163 NominationProposer {
1164 name: PersonName::new("Vries")
1165 .with_initials("M.")
1166 .with_first_name("Maria")
1167 .with_name_prefix("de")
1168 .into(),
1169 contact: NominationContact {
1170 mailing_address: MailingAddress::new(QualifyingAddress::Locality(
1171 QualifyingAddressLocality::new("Utrecht"),
1172 )),
1173 },
1174 job_title: StringValue::from_value(NominationJobTitle::DeputySubmitter),
1175 id: Some("PV001".into()),
1176 living_address: Some(
1177 LivingAddress::new("Utrecht").with_country_name_code("NL"),
1178 ),
1179 },
1180 ]))
1181 .build()
1182 .unwrap();
1183
1184 let xml = nomination.write_eml_root_str(true, true).unwrap();
1185 assert_eq!(
1186 xml,
1187 include_str!("../../test-files/nomination/eml210_construction_output.eml.xml")
1188 );
1189
1190 let parsed = Nomination::parse_eml(&xml, EMLParsingMode::Strict).unwrap();
1191 let xml2 = parsed.write_eml_root_str(true, true).unwrap();
1192 assert_eq!(xml, xml2);
1193 }
1194
1195 #[test]
1196 fn test_nomination_parse_and_write_roundtrip() {
1197 let doc = include_str!("../../test-files/nomination/eml210_test.eml.xml");
1198 let nomination = Nomination::parse_eml(doc, EMLParsingMode::Strict)
1199 .ok()
1200 .expect("Failed to parse EML 210 document");
1201
1202 assert_eq!(nomination.transaction_id.raw(), "1");
1203 assert!(nomination.managing_authority.is_some());
1204 assert_eq!(
1205 nomination.nomination_data.contest_identifier.name.as_ref(),
1206 "Test Contest"
1207 );
1208 assert_eq!(
1209 nomination
1210 .nomination_data
1211 .affiliation
1212 .registered_name
1213 .as_ref(),
1214 "Test Party"
1215 );
1216 assert!(!nomination.nomination_data.affiliation.candidates.is_empty());
1217 assert!(nomination.nomination_data.nominate.proposers.len() >= 2);
1218
1219 let xml_output = nomination
1220 .write_eml_root_str(true, true)
1221 .expect("Failed to write EML 210 document");
1222 let reparsed = Nomination::parse_eml(&xml_output, EMLParsingMode::Strict)
1223 .ok()
1224 .expect("Failed to re-parse written EML 210 document");
1225
1226 assert_eq!(
1227 reparsed.nomination_data.affiliation.registered_name,
1228 nomination.nomination_data.affiliation.registered_name
1229 );
1230 assert_eq!(
1231 reparsed.nomination_data.affiliation.candidates.len(),
1232 nomination.nomination_data.affiliation.candidates.len()
1233 );
1234 assert_eq!(
1235 reparsed.nomination_data.nominate.proposers.len(),
1236 nomination.nomination_data.nominate.proposers.len()
1237 );
1238 }
1239
1240 #[test]
1241 fn test_nomination_job_title_from_str() {
1242 assert_eq!(
1243 NominationJobTitle::from_eml_value("inleveraar"),
1244 Ok(NominationJobTitle::Submitter)
1245 );
1246 assert_eq!(
1247 NominationJobTitle::from_eml_value("plaatsvervanger van de inleveraar"),
1248 Ok(NominationJobTitle::DeputySubmitter)
1249 );
1250 assert_eq!(
1251 NominationJobTitle::from_eml_value(
1252 "gemachtigde voor het aangaan van lijstencombinaties"
1253 ),
1254 Ok(NominationJobTitle::CombinationRepresentative)
1255 );
1256 assert_eq!(
1257 NominationJobTitle::from_eml_value(
1258 "plaatsvervanger voor het aangaan van lijstencombinaties"
1259 ),
1260 Ok(NominationJobTitle::DeputyCombinationRepresentative)
1261 );
1262 assert_eq!(
1263 NominationJobTitle::from_eml_value("UNKNOWN"),
1264 Err(UnknownNominationJobTitleError("UNKNOWN".to_string()))
1265 );
1266 }
1267
1268 #[test]
1269 fn test_nomination_job_title_to_str() {
1270 assert_eq!(NominationJobTitle::Submitter.to_eml_value(), "inleveraar");
1271 assert_eq!(
1272 NominationJobTitle::DeputySubmitter.to_eml_value(),
1273 "plaatsvervanger van de inleveraar"
1274 );
1275 assert_eq!(
1276 NominationJobTitle::CombinationRepresentative.to_eml_value(),
1277 "gemachtigde voor het aangaan van lijstencombinaties"
1278 );
1279 assert_eq!(
1280 NominationJobTitle::DeputyCombinationRepresentative.to_eml_value(),
1281 "plaatsvervanger voor het aangaan van lijstencombinaties"
1282 );
1283 }
1284}