Skip to main content

eml_nl/documents/
nomination.rs

1//! Document variant for the EML_NL Nomination (`210`) document.
2
3use 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
32/// EML document ID for nominations.
33pub(crate) const EML_NOMINATION_ID: &str = "210";
34
35/// Representing a `210` document, containing a nomination.
36#[derive(Debug, Clone)]
37pub struct Nomination {
38    /// Transaction id of the document.
39    pub transaction_id: TransactionId,
40
41    /// Managing authority of the document, if present.
42    pub managing_authority: Option<ManagingAuthority>,
43
44    /// Issue date of the document.
45    pub issue_date: IssueDate,
46
47    /// Creation date and time of the document.
48    pub creation_date_time: CreationDateTime,
49
50    /// Canonicalization method used in this document, if present.
51    pub canonicalization_method: Option<CanonicalizationMethod>,
52
53    /// The nomination data contained in this document.
54    pub nomination_data: NominationData,
55}
56
57impl Nomination {
58    /// Create a new builder for the [`Nomination`] document.
59    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/// Builder for the [`Nomination`] document.
92#[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    /// Create a new builder for the [`Nomination`] document.
108    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    /// Set the transaction id for the document.
124    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    /// Set the managing authority for the document.
130    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    /// Set the issue date for the document.
136    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    /// Set the creation date and time for the document.
142    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    /// Set the canonicalization method for the document.
148    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    /// Set the nomination data for the document directly.
157    ///
158    /// You may either set the entire nomination data at once using this
159    /// method, or use any of [`Self::election_identifier`],
160    /// [`Self::contest_identifier`], [`Self::affiliation`] and/or
161    /// [`Self::nominate`] to construct the individual components.
162    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    /// Set the election identifier for the contained Nomination element.
168    ///
169    /// This only has effect if the nomination data was not set directly using
170    /// [`Self::nomination_data`].
171    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    /// Set the contest identifier for the contained Nomination element.
180    ///
181    /// This only has effect if the nomination data was not set directly using
182    /// [`Self::nomination_data`].
183    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    /// Set the affiliation for the contained Nomination element.
192    ///
193    /// This only has effect if the nomination data was not set directly using
194    /// [`Self::nomination_data`].
195    pub fn affiliation(mut self, affiliation: impl Into<NominationAffiliation>) -> Self {
196        self.affiliation = Some(affiliation.into());
197        self
198    }
199
200    /// Set the nominate element for the contained Nomination element.
201    ///
202    /// This only has effect if the nomination data was not set directly using
203    /// [`Self::nomination_data`].
204    pub fn nominate(mut self, nominate: impl Into<NominationNominate>) -> Self {
205        self.nominate = Some(nominate.into());
206        self
207    }
208
209    /// Build the `Nomination` document, returning an error if any required fields are missing.
210    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            // Note: we don't output the CanonicalizationMethod because we aren't canonicalizing our output
289            // .child_elem_option(
290            //     CanonicalizationMethod::EML_NAME,
291            //     self.canonicalization_method.as_ref(),
292            // )?
293            .child_elem(NominationData::EML_NAME, &self.nomination_data)?
294            .finish()
295    }
296}
297
298/// The `<Nomination>` element containing election, contest, affiliation and proposer data.
299#[derive(Debug, Clone)]
300pub struct NominationData {
301    /// The election identifier.
302    pub election_identifier: NominationElectionIdentifier,
303
304    /// The contest identifier.
305    pub contest_identifier: NominationContestIdentifier,
306
307    /// The affiliation with its candidates.
308    pub affiliation: NominationAffiliation,
309
310    /// The proposers who nominate this list.
311    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/// Identifier for the election in a nomination document.
343#[derive(Debug, Clone)]
344pub struct NominationElectionIdentifier {
345    /// Id of the election
346    pub id: StringValue<ElectionId>,
347
348    /// Name of the election
349    pub name: Option<Box<str>>,
350
351    /// Category of the election
352    pub category: StringValue<ElectionCategory>,
353
354    /// Subcategory of the election
355    pub subcategory: Option<StringValue<ElectionSubcategory>>,
356
357    /// The (top level) region where the election takes place.
358    pub domain: Option<ElectionDomain>,
359
360    /// Date of the election
361    pub election_date: StringValue<XsDate>,
362
363    /// Nomination date for the election
364    pub nomination_date: StringValue<XsDate>,
365}
366
367impl NominationElectionIdentifier {
368    /// Create a new Election Identifier builder
369    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/// Contest identifier for a nomination document (with mandatory ContestName).
445#[derive(Debug, Clone)]
446pub struct NominationContestIdentifier {
447    /// Id of the contest.
448    pub id: StringValue<ContestId>,
449
450    /// Name of the contest (mandatory in 210).
451    pub name: Box<str>,
452}
453
454impl NominationContestIdentifier {
455    /// Create a new `NominationContestIdentifier`.
456    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/// An affiliation in a nomination document.
489///
490/// In EML 210, the affiliation identifier has no `Id` attribute (it is prohibited),
491/// and the `RegisteredName` is mandatory.
492#[derive(Debug, Clone)]
493pub struct NominationAffiliation {
494    /// The registered name of the affiliation (Id is prohibited in 210).
495    pub registered_name: Box<str>,
496
497    /// The affiliation type.
498    pub affiliation_type: StringValue<AffiliationType>,
499
500    /// The list data of the affiliation.
501    pub list_data: ListData,
502
503    /// The candidates of the affiliation.
504    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        // The AffiliationIdentifier in 210 has no Id attribute and a required RegisteredName
512        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/// A candidate in a nomination document.
575///
576/// In EML 210, `Gender` and `QualifyingAddress` are required (unlike in 230b/230c
577/// where they are optional). Additional fields like `Contact`, `Agent`,
578/// `DateOfBirthAnnex` and `NationalIdentificationNumber` are also supported.
579#[derive(Debug, Clone)]
580pub struct NominationCandidate {
581    /// The candidate identifier.
582    pub identifier: CandidateIdentifier,
583
584    /// The full name of the candidate.
585    pub full_name: PersonNameStructure,
586
587    /// The date of birth of the candidate, if present.
588    pub date_of_birth: Option<StringValue<XsDate>>,
589
590    /// The gender of the candidate (required in 210).
591    pub gender: StringValue<Gender>,
592
593    /// The qualifying address of the candidate (required in 210).
594    pub qualifying_address: QualifyingAddress,
595
596    /// Contact details for the candidate, if present.
597    pub contact: Option<NominationContact>,
598
599    /// Agent details for the candidate, if present.
600    pub agent: Option<NominationAgent>,
601
602    /// Alternative date of birth representation when exact date is unknown.
603    pub date_of_birth_annex: Option<Box<str>>,
604
605    /// National identification number (e.g. BSN in the Netherlands).
606    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/// Contact details (containing a mailing address).
659#[derive(Debug, Clone)]
660pub struct NominationContact {
661    /// The mailing address.
662    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/// A mailing address, structured as a qualifying address (Locality or Country).
682#[derive(Debug, Clone)]
683pub struct MailingAddress {
684    /// The address content (Locality or Country).
685    pub address: QualifyingAddress,
686}
687
688impl MailingAddress {
689    /// Create a new mailing address with a locality.
690    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/// An agent for a candidate.
756#[derive(Debug, Clone)]
757pub struct NominationAgent {
758    /// The role of the agent (e.g. "H10" or "H10a").
759    pub role: Option<String>,
760
761    /// The agent's name.
762    pub agent_identifier: AgentIdentifier,
763
764    /// Contact details for the agent, if present.
765    pub contact: Option<NominationContact>,
766
767    /// The living address of the agent.
768    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/// Job title used for a proposer in a nomination document.
794#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
795pub enum NominationJobTitle {
796    /// inleveraar
797    Submitter,
798    /// plaatsvervanger van de inleveraar
799    DeputySubmitter,
800    /// gemachtigde voor het aangaan van lijstencombinaties
801    CombinationRepresentative,
802    /// plaatsvervanger voor het aangaan van lijstencombinaties
803    DeputyCombinationRepresentative,
804}
805
806impl NominationJobTitle {
807    /// Create a new NominationJobTitle from a string, validating its format.
808    pub fn new(s: impl AsRef<str>) -> Result<Self, EMLError> {
809        Self::from_eml_value(s).wrap_value_error()
810    }
811
812    /// Create a [`NominationJobTitle`] from a `&str`, if possible.
813    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    /// Get the `&str` representation of this [`NominationJobTitle`].
829    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/// Error returned when an unknown nomination job title string is encountered.
844#[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/// Agent identifier containing the agent's name.
864#[derive(Debug, Clone)]
865pub struct AgentIdentifier {
866    /// The agent's name.
867    pub agent_name: PersonNameStructure,
868}
869
870impl AgentIdentifier {
871    /// Create a new `AgentIdentifier`.
872    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/// A living address (kr:LivingAddress).
897#[derive(Debug, Clone)]
898pub struct LivingAddress {
899    /// The locality name.
900    pub locality_name: Box<str>,
901
902    /// The country name code, if present.
903    pub country_name_code: Option<Box<str>>,
904}
905
906impl LivingAddress {
907    /// Create a new `LivingAddress`.
908    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    /// Set the country name code.
916    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/// The `<Nominate>` element containing proposers.
948#[derive(Debug, Clone)]
949pub struct NominationNominate {
950    /// The proposers (minimum 2 required by schema).
951    pub proposers: Vec<NominationProposer>,
952}
953
954impl NominationNominate {
955    /// Create a new `NominationNominate` with the given proposers.
956    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/// A proposer in a nomination document.
990#[derive(Debug, Clone)]
991pub struct NominationProposer {
992    /// The proposer's name.
993    pub name: PersonNameStructure,
994
995    /// Contact details for the proposer (required).
996    pub contact: NominationContact,
997
998    /// The job title of the proposer.
999    ///
1000    /// Valid values: "inleveraar", "plaatsvervanger van de inleveraar",
1001    /// "gemachtigde voor het aangaan van lijstencombinaties",
1002    /// "plaatsvervanger voor het aangaan van lijstencombinaties"
1003    pub job_title: StringValue<NominationJobTitle>,
1004
1005    /// Optional identifier for the proposer (mandatory if deputy).
1006    pub id: Option<Box<str>>,
1007
1008    /// The living address of the proposer, if present.
1009    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}