Skip to main content

eml_nl/documents/
polling_stations.rs

1//! Document variant for the EML_NL Polling Stations (`110b`) document.
2
3use std::{num::NonZeroU64, str::FromStr, sync::LazyLock};
4
5use regex::Regex;
6use thiserror::Error;
7
8use crate::{
9    EML_SCHEMA_VERSION, EMLError, EMLValueResultExt, NS_EML, NS_KR,
10    common::{
11        CanonicalizationMethod, ContestIdentifier, ContestIdentifierGeen, CreationDateTime,
12        ElectionDomain, IssueDate, LocalityName, ManagingAuthority, PostalCode,
13        ReportingUnitIdentifier, TransactionId,
14    },
15    documents::{ElectionIdentifierBuilder, accepted_root},
16    error::{EMLErrorKind, EMLResultExt},
17    io::{
18        EMLElement, EMLElementReader, EMLElementWriter, OwnedQualifiedName, QualifiedName,
19        collect_struct,
20    },
21    utils::{
22        ElectionCategory, ElectionId, ElectionSubcategory, StringValue, StringValueData,
23        VotingChannelType, VotingMethod, XsDate, XsDateOrDateTime, XsDateTime,
24    },
25};
26
27pub(crate) const EML_POLLING_STATIONS_ID: &str = "110b";
28
29/// Representing a `110b` document, containing polling stations.
30#[derive(Debug, Clone)]
31pub struct PollingStations {
32    /// Transaction id of the document.
33    pub transaction_id: TransactionId,
34
35    /// Managing authority of the document.
36    pub managing_authority: ManagingAuthority,
37
38    /// Issue date of the document.
39    pub issue_date: Option<IssueDate>,
40
41    /// Creation date and time of the document.
42    pub creation_date_time: CreationDateTime,
43
44    /// Canonicalization method used in this document, if present.
45    pub canonicalization_method: Option<CanonicalizationMethod>,
46
47    /// Election event containing the polling stations.
48    pub election_event: PollingStationsElectionEvent,
49}
50
51impl PollingStations {
52    /// Create a new builder for constructing a [`PollingStations`] document.
53    pub fn builder() -> PollingStationsBuilder {
54        PollingStationsBuilder::new()
55    }
56}
57
58impl FromStr for PollingStations {
59    type Err = EMLError;
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        use crate::io::EMLRead as _;
63        Self::parse_eml(s, crate::io::EMLParsingMode::Strict).ok()
64    }
65}
66
67impl TryFrom<&str> for PollingStations {
68    type Error = EMLError;
69
70    fn try_from(value: &str) -> Result<Self, Self::Error> {
71        use crate::io::EMLRead as _;
72        Self::parse_eml(value, crate::io::EMLParsingMode::Strict).ok()
73    }
74}
75
76impl TryFrom<PollingStations> for String {
77    type Error = EMLError;
78
79    fn try_from(value: PollingStations) -> Result<Self, Self::Error> {
80        use crate::io::EMLWrite as _;
81        value.write_eml_root_str(true, true)
82    }
83}
84
85/// Builder for the [`PollingStations`] document.
86#[derive(Debug, Clone)]
87pub struct PollingStationsBuilder {
88    transaction_id: Option<TransactionId>,
89    managing_authority: Option<ManagingAuthority>,
90    issue_date: Option<IssueDate>,
91    creation_date_time: Option<CreationDateTime>,
92    canonicalization_method: Option<CanonicalizationMethod>,
93    election_event: Option<PollingStationsElectionEvent>,
94    election_identifier: Option<PollingStationsElectionIdentifier>,
95    contests: Vec<PollingStationsContest>,
96}
97
98impl PollingStationsBuilder {
99    /// Create a new builder for the [`PollingStations`] document.
100    pub fn new() -> Self {
101        Self {
102            transaction_id: None,
103            managing_authority: None,
104            issue_date: None,
105            creation_date_time: None,
106            canonicalization_method: None,
107            election_event: None,
108            election_identifier: None,
109            contests: vec![],
110        }
111    }
112
113    /// Set the transaction id of the document.
114    pub fn transaction_id(mut self, transaction_id: impl Into<TransactionId>) -> Self {
115        self.transaction_id = Some(transaction_id.into());
116        self
117    }
118
119    /// Set the managing authority of the document.
120    pub fn managing_authority(mut self, managing_authority: impl Into<ManagingAuthority>) -> Self {
121        self.managing_authority = Some(managing_authority.into());
122        self
123    }
124
125    /// Set the issue date of the document.
126    pub fn issue_date(mut self, issue_date: impl Into<XsDateOrDateTime>) -> Self {
127        self.issue_date = Some(IssueDate::new(issue_date.into()));
128        self
129    }
130
131    /// Set the creation date and time of the document.
132    pub fn creation_date_time(mut self, creation_date_time: impl Into<XsDateTime>) -> Self {
133        self.creation_date_time = Some(CreationDateTime::new(creation_date_time.into()));
134        self
135    }
136
137    /// Set the canonicalization method for the document.
138    pub fn canonicalization_method(
139        mut self,
140        canonicalization_method: impl Into<CanonicalizationMethod>,
141    ) -> Self {
142        self.canonicalization_method = Some(canonicalization_method.into());
143        self
144    }
145
146    /// Set the election event containing the polling stations.
147    ///
148    /// You may either set the entire election event at once using this method,
149    /// or use any of [`Self::election_identifier`], [`Self::contests`] and/or
150    /// [`Self::push_contest`] to set the individual components of the
151    /// election event and allow this builder to construct them for you.
152    pub fn election_event(
153        mut self,
154        election_event: impl Into<PollingStationsElectionEvent>,
155    ) -> Self {
156        self.election_event = Some(election_event.into());
157        self
158    }
159
160    /// Set the election identifier for the contained Election element.
161    ///
162    /// This only has effect if the election event was not set directly using
163    /// [`Self::election_event`].
164    pub fn election_identifier(
165        mut self,
166        election_identifier: impl Into<PollingStationsElectionIdentifier>,
167    ) -> Self {
168        self.election_identifier = Some(election_identifier.into());
169        self
170    }
171
172    /// Set the list of contests within the election. This will replace any
173    /// existing contests set using this method or the [`Self::push_contest`]
174    /// method.
175    ///
176    /// This only has effect if the election event was not set directly using
177    /// [`Self::election_event`].
178    pub fn contests(mut self, contests: impl Into<Vec<PollingStationsContest>>) -> Self {
179        self.contests = contests.into();
180        self
181    }
182
183    /// Add a contest to the election.
184    ///
185    /// This only has effect if the election event was not set directly using
186    /// [`Self::election_event`].
187    pub fn push_contest(mut self, contest: impl Into<PollingStationsContest>) -> Self {
188        self.contests.push(contest.into());
189        self
190    }
191
192    /// Build the [`PollingStations`] document, returning any errors if required fields are missing.
193    pub fn build(self) -> Result<PollingStations, EMLError> {
194        Ok(PollingStations {
195            transaction_id: self
196                .transaction_id
197                .ok_or(EMLErrorKind::MissingBuildProperty("transaction_id").without_span())?,
198            managing_authority: self
199                .managing_authority
200                .ok_or(EMLErrorKind::MissingBuildProperty("managing_authority").without_span())?,
201            issue_date: self.issue_date,
202            creation_date_time: self
203                .creation_date_time
204                .ok_or(EMLErrorKind::MissingBuildProperty("creation_date_time").without_span())?,
205            canonicalization_method: self.canonicalization_method,
206            election_event: self.election_event.map_or_else(
207                || {
208                    if self.contests.is_empty() {
209                        return Err(EMLErrorKind::MissingBuildProperty("contests").without_span());
210                    }
211
212                    let election = PollingStationsElection::new(self.election_identifier.ok_or(
213                        EMLErrorKind::MissingBuildProperty("election_identifier").without_span(),
214                    )?)
215                    .with_contests(self.contests);
216
217                    let event = PollingStationsElectionEvent::new(election);
218
219                    Ok(event)
220                },
221                Ok,
222            )?,
223        })
224    }
225}
226
227impl Default for PollingStationsBuilder {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233impl EMLElement for PollingStations {
234    const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("EML", Some(NS_EML));
235
236    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
237        accepted_root(elem)?;
238
239        let document_id = elem.attribute_value_req(("Id", None))?;
240        if document_id != EML_POLLING_STATIONS_ID {
241            return Err(EMLErrorKind::InvalidDocumentType(
242                EML_POLLING_STATIONS_ID,
243                document_id.to_string(),
244            ))
245            .with_span(elem.span());
246        }
247
248        Ok(collect_struct!(elem, PollingStations {
249            transaction_id: TransactionId::EML_NAME => |elem| TransactionId::read_eml(elem)?,
250            managing_authority: ManagingAuthority::EML_NAME => |elem| ManagingAuthority::read_eml(elem)?,
251            issue_date as Option: IssueDate::EML_NAME => |elem| IssueDate::read_eml(elem)?,
252            creation_date_time: CreationDateTime::EML_NAME => |elem| CreationDateTime::read_eml(elem)?,
253            canonicalization_method as Option: CanonicalizationMethod::EML_NAME => |elem| CanonicalizationMethod::read_eml(elem)?,
254            election_event: PollingStationsElectionEvent::EML_NAME => |elem| PollingStationsElectionEvent::read_eml(elem)?,
255        }))
256    }
257
258    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
259        writer
260            .attr(("Id", None), EML_POLLING_STATIONS_ID)?
261            .attr(("SchemaVersion", None), EML_SCHEMA_VERSION)?
262            .child_elem(TransactionId::EML_NAME, &self.transaction_id)?
263            .child_elem(ManagingAuthority::EML_NAME, &self.managing_authority)?
264            .child_elem_option(IssueDate::EML_NAME, self.issue_date.as_ref())?
265            .child_elem(CreationDateTime::EML_NAME, &self.creation_date_time)?
266            // Note: we don't output the CanonicalizationMethod because we aren't canonicalizing our output
267            // .child_elem_option(
268            //     CanonicalizationMethod::EML_NAME,
269            //     self.canonicalization_method.as_ref(),
270            // )?
271            .child_elem(PollingStationsElectionEvent::EML_NAME, &self.election_event)?
272            .finish()?;
273
274        Ok(())
275    }
276}
277
278/// Election event containing polling stations.
279#[derive(Debug, Clone)]
280pub struct PollingStationsElectionEvent {
281    /// Election details.
282    pub election: PollingStationsElection,
283}
284
285impl PollingStationsElectionEvent {
286    /// Create a new election event containing the given election.
287    pub fn new(election: impl Into<PollingStationsElection>) -> Self {
288        PollingStationsElectionEvent {
289            election: election.into(),
290        }
291    }
292}
293
294impl From<PollingStationsElection> for PollingStationsElectionEvent {
295    fn from(value: PollingStationsElection) -> Self {
296        PollingStationsElectionEvent::new(value)
297    }
298}
299
300impl EMLElement for PollingStationsElectionEvent {
301    const EML_NAME: QualifiedName<'_, '_> =
302        QualifiedName::from_static("ElectionEvent", Some(NS_EML));
303
304    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError>
305    where
306        Self: Sized,
307    {
308        Ok(collect_struct!(elem, PollingStationsElectionEvent {
309            id as None: ("EventIdentifier", NS_EML) => |elem| elem.skip().map(|_| ())?,
310            election: PollingStationsElection::EML_NAME => |elem| PollingStationsElection::read_eml(elem)?,
311        }))
312    }
313
314    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
315        writer
316            .child(("EventIdentifier", NS_EML), |w| w.empty())?
317            .child_elem(PollingStationsElection::EML_NAME, &self.election)?
318            .finish()
319    }
320}
321
322/// Election definition containing polling stations.
323#[derive(Debug, Clone)]
324pub struct PollingStationsElection {
325    /// Identifier of the election.
326    pub identifier: PollingStationsElectionIdentifier,
327
328    /// Contests containing the polling stations.
329    pub contests: Vec<PollingStationsContest>,
330}
331
332impl PollingStationsElection {
333    /// Create a new election within the polling stations document.
334    pub fn new(identifier: impl Into<PollingStationsElectionIdentifier>) -> Self {
335        PollingStationsElection {
336            identifier: identifier.into(),
337            contests: vec![],
338        }
339    }
340
341    /// Set the contests within the election. This will replace any existing
342    /// contests set using this method or the [`Self::push_contest`] method.
343    pub fn with_contests(mut self, contests: impl Into<Vec<PollingStationsContest>>) -> Self {
344        self.contests = contests.into();
345        self
346    }
347
348    /// Add a contest to the election.
349    pub fn push_contest(mut self, contest: impl Into<PollingStationsContest>) -> Self {
350        self.contests.push(contest.into());
351        self
352    }
353}
354
355impl EMLElement for PollingStationsElection {
356    const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Election", Some(NS_EML));
357
358    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError>
359    where
360        Self: Sized,
361    {
362        Ok(collect_struct!(elem, PollingStationsElection {
363            identifier: PollingStationsElectionIdentifier::EML_NAME => |elem| PollingStationsElectionIdentifier::read_eml(elem)?,
364            contests as Vec: PollingStationsContest::EML_NAME => |elem| PollingStationsContest::read_eml(elem)?,
365        }))
366    }
367
368    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
369        writer
370            .child_elem(
371                PollingStationsElectionIdentifier::EML_NAME,
372                &self.identifier,
373            )?
374            .child_elems(PollingStationsContest::EML_NAME, &self.contests)?
375            .finish()
376    }
377}
378
379/// Identifier of an election in the polling stations document.
380#[derive(Debug, Clone)]
381pub struct PollingStationsElectionIdentifier {
382    /// Election id.
383    pub id: StringValue<ElectionId>,
384
385    /// Election name, if present.
386    pub name: Option<Box<str>>,
387
388    /// Election category.
389    pub category: StringValue<ElectionCategory>,
390
391    /// Election subcategory, if present.
392    pub subcategory: Option<StringValue<ElectionSubcategory>>,
393
394    /// The (top level) region where the election takes place.
395    pub domain: Option<ElectionDomain>,
396
397    /// Date of the election
398    pub election_date: StringValue<XsDate>,
399}
400
401impl PollingStationsElectionIdentifier {
402    /// Create a new builder for constructing a [`PollingStationsElectionIdentifier`].
403    pub fn builder() -> ElectionIdentifierBuilder {
404        ElectionIdentifierBuilder::new()
405    }
406}
407
408impl EMLElement for PollingStationsElectionIdentifier {
409    const EML_NAME: QualifiedName<'_, '_> =
410        QualifiedName::from_static("ElectionIdentifier", Some(NS_EML));
411
412    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
413        struct PollingStationsElectionIdentifierInternal {
414            id: StringValue<ElectionId>,
415            name: Option<Box<str>>,
416            category: StringValue<ElectionCategory>,
417            subcategory: Option<StringValue<ElectionSubcategory>>,
418            domain: Option<ElectionDomain>,
419            election_date: Option<StringValue<XsDate>>,
420            election_date_eml: Option<StringValue<XsDate>>,
421        }
422
423        let data = collect_struct!(
424            elem,
425            PollingStationsElectionIdentifierInternal {
426                id: elem.string_value_attr("Id", None)?,
427                name as Option: ("ElectionName", NS_EML) => |elem| elem.text_without_children()?,
428                category: ("ElectionCategory", NS_EML) => |elem| elem.string_value()?,
429                subcategory as Option: ("ElectionSubcategory", NS_KR) => |elem| elem.string_value()?,
430                domain as Option: ElectionDomain::EML_NAME => |elem| ElectionDomain::read_eml(elem)?,
431                election_date as Option: ("ElectionDate", NS_KR) => |elem| elem.string_value()?,
432                election_date_eml as Option: ("ElectionDate", NS_EML) => |elem| {
433                    if elem.parsing_mode().is_strict() {
434                        let err = EMLErrorKind::InvalidElectionDateNamespace.with_span(elem.span());
435                        return Err(err);
436                    } else {
437                        elem.push_err(EMLErrorKind::InvalidElectionDateNamespace.with_span(elem.span()));
438                    }
439                    elem.string_value()?
440                },
441            }
442        );
443
444        let election_date = match (data.election_date, data.election_date_eml) {
445            (Some(date), _) => date,
446            (None, Some(date)) => date,
447            (None, None) => {
448                return Err(
449                    EMLErrorKind::MissingElement(OwnedQualifiedName::from_static(
450                        "ElectionDate",
451                        Some(NS_KR),
452                    ))
453                    .with_span(elem.full_span()),
454                );
455            }
456        };
457
458        Ok(PollingStationsElectionIdentifier {
459            id: data.id,
460            name: data.name,
461            category: data.category,
462            subcategory: data.subcategory,
463            domain: data.domain,
464            election_date,
465        })
466    }
467
468    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
469        writer
470            .attr("Id", self.id.raw().as_ref())?
471            .child_option(
472                ("ElectionName", NS_EML),
473                self.name.as_ref(),
474                |elem, value| elem.text(value)?.finish(),
475            )?
476            .child(("ElectionCategory", NS_EML), |elem| {
477                elem.text(self.category.raw().as_ref())?.finish()
478            })?
479            .child_option(
480                ("ElectionSubcategory", NS_KR),
481                self.subcategory.as_ref(),
482                |elem, value| elem.text(value.raw().as_ref())?.finish(),
483            )?
484            .child_elem_option(ElectionDomain::EML_NAME, self.domain.as_ref())?
485            .child(("ElectionDate", NS_KR), |elem| {
486                elem.text(self.election_date.raw().as_ref())?.finish()
487            })?
488            .finish()
489    }
490}
491
492/// Contest containing polling stations.
493#[derive(Debug, Clone)]
494pub struct PollingStationsContest {
495    /// Identifier for the contest.
496    pub identifier: ContestIdentifierGeen,
497
498    /// Reporting unit for the contest.
499    pub reporting_unit: PollingStationsReportingUnit,
500
501    /// Voting method used in the contest.
502    pub voting_method: StringValue<VotingMethod>,
503
504    /// Maximum number of votes allowed.
505    /// EML specifies the default value as 1, so an empty tag will be parsed as 1 and vice versa.
506    pub max_votes: StringValue<NonZeroU64>,
507
508    /// List of polling places in this contest.
509    pub polling_places: Vec<PollingPlace>,
510}
511
512impl PollingStationsContest {
513    /// Create a new builder for constructing a [`PollingStationsContest`].
514    pub fn builder() -> PollingStationsContestBuilder {
515        PollingStationsContestBuilder::new()
516    }
517}
518
519/// Builder for the [`PollingStationsContest`] struct.
520#[derive(Debug, Clone)]
521pub struct PollingStationsContestBuilder {
522    reporting_unit: Option<PollingStationsReportingUnit>,
523    voting_method: Option<StringValue<VotingMethod>>,
524    max_votes: Option<StringValue<NonZeroU64>>,
525    polling_places: Vec<PollingPlace>,
526}
527
528impl PollingStationsContestBuilder {
529    /// Create a new builder for constructing a [`PollingStationsContest`].
530    pub fn new() -> Self {
531        Self {
532            reporting_unit: None,
533            voting_method: None,
534            max_votes: None,
535            polling_places: vec![],
536        }
537    }
538
539    /// Set the reporting unit for the contest.
540    pub fn reporting_unit(
541        mut self,
542        reporting_unit: impl Into<PollingStationsReportingUnit>,
543    ) -> Self {
544        self.reporting_unit = Some(reporting_unit.into());
545        self
546    }
547
548    /// Set the voting method used in the contest.
549    pub fn voting_method(mut self, voting_method: impl Into<VotingMethod>) -> Self {
550        self.voting_method = Some(StringValue::from_value(voting_method.into()));
551        self
552    }
553
554    /// Set the maximum number of votes allowed in the contest.
555    /// EML specifies the default value as 1, so providing the value of 1 will result in an empty xml tag.
556    pub fn max_votes(mut self, max_votes: impl Into<NonZeroU64>) -> Self {
557        self.max_votes = Some(StringValue::from_value(max_votes.into()));
558        self
559    }
560
561    /// Set the list of polling places in the contest. This will replace any
562    /// existing polling places set using this method or the [`Self::push_polling_place`]
563    /// method.
564    pub fn polling_places(mut self, polling_places: impl Into<Vec<PollingPlace>>) -> Self {
565        self.polling_places = polling_places.into();
566        self
567    }
568
569    /// Add a polling place to the contest.
570    pub fn push_polling_place(mut self, polling_place: impl Into<PollingPlace>) -> Self {
571        self.polling_places.push(polling_place.into());
572        self
573    }
574
575    /// Build the [`PollingStationsContest`], returning any errors if required fields are missing.
576    pub fn build(self) -> Result<PollingStationsContest, EMLError> {
577        if self.polling_places.is_empty() {
578            return Err(EMLErrorKind::MissingBuildProperty("polling_places").without_span());
579        }
580
581        let voting_method = self
582            .voting_method
583            .ok_or(EMLErrorKind::MissingBuildProperty("voting_method").without_span())?;
584        if let Ok(vm) = voting_method.copied_value()
585            && vm != VotingMethod::SPV
586        {
587            return Err(EMLErrorKind::UnsupportedVotingMethod).without_span();
588        }
589
590        Ok(PollingStationsContest {
591            identifier: ContestIdentifierGeen::default(),
592            reporting_unit: self
593                .reporting_unit
594                .ok_or(EMLErrorKind::MissingBuildProperty("reporting_unit").without_span())?,
595            voting_method,
596            max_votes: self
597                .max_votes
598                .ok_or(EMLErrorKind::MissingBuildProperty("max_votes").without_span())?,
599            polling_places: self.polling_places,
600        })
601    }
602}
603
604impl Default for PollingStationsContestBuilder {
605    fn default() -> Self {
606        Self::new()
607    }
608}
609
610impl EMLElement for PollingStationsContest {
611    const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Contest", Some(NS_EML));
612
613    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
614        struct PollingStationsContestInternal {
615            pub identifier: Option<ContestIdentifierGeen>,
616            pub reporting_unit: PollingStationsReportingUnit,
617            pub voting_method: StringValue<VotingMethod>,
618            pub max_votes: StringValue<NonZeroU64>,
619            pub polling_places: Vec<PollingPlace>,
620        }
621
622        let data = collect_struct!(elem, PollingStationsContestInternal {
623            identifier as Option: ContestIdentifierGeen::EML_NAME => |elem| ContestIdentifierGeen::read_eml(elem)?,
624            reporting_unit: PollingStationsReportingUnit::EML_NAME => |elem| PollingStationsReportingUnit::read_eml(elem)?,
625            voting_method: ("VotingMethod", NS_EML) => |elem| {
626                let value = elem.string_value_opt()?;
627                if let Some(value) = value {
628                    value
629                } else {
630                    let err = EMLErrorKind::MissingElementValue(OwnedQualifiedName::from_static("VotingMethod", Some(NS_EML)))
631                        .with_span(elem.full_span());
632                    if elem.parsing_mode().is_strict() {
633                        return Err(err);
634                    } else {
635                        elem.push_err(err);
636                        StringValue::from_value(VotingMethod::SPV)
637                    }
638                }
639            },
640            max_votes: ("MaxVotes", NS_EML) => |elem| {
641                // Default value of MaxVotes in EML is 1
642                let text = elem.text_without_children_opt()?.unwrap_or_else(|| "1".into());
643                elem.string_value_from_text(text, None, elem.full_span())?
644            },
645            polling_places as Vec: PollingPlace::EML_NAME => |elem| PollingPlace::read_eml(elem)?,
646        });
647
648        // Some municipalities omit the ContestIdentifier element, even though it is required.
649        let identifier = if let Some(identifier) = data.identifier {
650            identifier
651        } else {
652            let err = EMLErrorKind::MissingContenstIdentifier.with_span(elem.span());
653            if elem.parsing_mode().is_strict() {
654                return Err(err);
655            } else {
656                elem.push_err(err);
657                ContestIdentifierGeen::default()
658            }
659        };
660
661        // Technically there should be at least one polling place
662        if data.polling_places.is_empty() {
663            let err = EMLErrorKind::MissingElement(PollingPlace::EML_NAME.as_owned())
664                .with_span(elem.full_span());
665            if elem.parsing_mode().is_strict() {
666                return Err(err);
667            } else {
668                elem.push_err(err);
669            }
670        }
671
672        // Only SPV voting method is supported
673        if let Ok(vm) = data.voting_method.copied_value()
674            && vm != VotingMethod::SPV
675        {
676            let err = EMLErrorKind::UnsupportedVotingMethod.with_span(elem.full_span());
677            if elem.parsing_mode().is_strict() {
678                return Err(err);
679            } else {
680                elem.push_err(err);
681            }
682        }
683
684        Ok(PollingStationsContest {
685            identifier,
686            reporting_unit: data.reporting_unit,
687            voting_method: data.voting_method,
688            max_votes: data.max_votes,
689            polling_places: data.polling_places,
690        })
691    }
692
693    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
694        writer
695            .child_elem(ContestIdentifier::EML_NAME, &self.identifier)?
696            .child_elem(PollingStationsReportingUnit::EML_NAME, &self.reporting_unit)?
697            .child(("VotingMethod", NS_EML), |elem| {
698                elem.text(self.voting_method.raw().as_ref())?.finish()
699            })?
700            .child(("MaxVotes", NS_EML), |elem| {
701                let raw_text = self.max_votes.raw();
702                // Default value of MaxVotes in EML is 1
703                if raw_text == "1" {
704                    elem.empty()
705                } else {
706                    elem.text(raw_text.as_ref())?.finish()
707                }
708            })?
709            .child_elems(PollingPlace::EML_NAME, &self.polling_places)?
710            .finish()
711    }
712}
713
714/// Reporting unit for the contest
715#[derive(Debug, Clone)]
716pub struct PollingStationsReportingUnit {
717    /// Identifier of the reporting unit.
718    pub identifier: ReportingUnitIdentifier,
719}
720
721impl PollingStationsReportingUnit {
722    /// Create a new reporting unit for the polling stations document.
723    pub fn new(identifier: impl Into<ReportingUnitIdentifier>) -> Self {
724        PollingStationsReportingUnit {
725            identifier: identifier.into(),
726        }
727    }
728}
729
730impl From<ReportingUnitIdentifier> for PollingStationsReportingUnit {
731    fn from(value: ReportingUnitIdentifier) -> Self {
732        PollingStationsReportingUnit::new(value)
733    }
734}
735
736impl EMLElement for PollingStationsReportingUnit {
737    const EML_NAME: QualifiedName<'_, '_> =
738        QualifiedName::from_static("ReportingUnit", Some(NS_EML));
739
740    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
741        Ok(collect_struct!(elem, PollingStationsReportingUnit {
742            identifier: ReportingUnitIdentifier::EML_NAME => |elem| ReportingUnitIdentifier::read_eml(elem)?,
743        }))
744    }
745
746    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
747        writer
748            .child_elem(ReportingUnitIdentifier::EML_NAME, &self.identifier)?
749            .finish()
750    }
751}
752
753/// A polling place in the polling stations document.
754#[derive(Debug, Clone)]
755pub struct PollingPlace {
756    /// Voting channel used at this polling place.
757    pub channel: StringValue<VotingChannelType>,
758
759    /// Physical location of the polling place.
760    pub physical_location: PhysicalLocation,
761}
762
763impl PollingPlace {
764    /// Create a new builder for constructing a [`PollingPlace`].
765    pub fn builder() -> PollingPlaceBuilder {
766        PollingPlaceBuilder::new()
767    }
768}
769
770/// Builder for the [`PollingPlace`] struct.
771#[derive(Debug, Clone)]
772pub struct PollingPlaceBuilder {
773    channel: Option<StringValue<VotingChannelType>>,
774    polling_station_id: Option<StringValue<PhysicalLocationPollingStationId>>,
775    polling_station_data: Option<Box<str>>,
776    locality_name: Option<LocalityName>,
777    postal_code: Option<PostalCode>,
778}
779
780impl PollingPlaceBuilder {
781    /// Create a new builder for constructing a [`PollingPlace`].
782    pub fn new() -> Self {
783        Self {
784            channel: None,
785            polling_station_id: None,
786            polling_station_data: None,
787            locality_name: None,
788            postal_code: None,
789        }
790    }
791
792    /// Set the voting channel used at the polling place.
793    pub fn channel(mut self, channel: impl Into<VotingChannelType>) -> Self {
794        self.channel = Some(StringValue::from_value(channel.into()));
795        self
796    }
797
798    /// Set the identifier of the polling station at the polling place.
799    pub fn polling_station_id(mut self, id: impl Into<PhysicalLocationPollingStationId>) -> Self {
800        self.polling_station_id = Some(StringValue::from_value(id.into()));
801        self
802    }
803
804    /// Set the additional data of the polling station at the polling place.
805    pub fn polling_station_data(self, data: impl Into<Box<str>>) -> Self {
806        self.polling_station_data_option(Some(data))
807    }
808
809    /// Set the additional data of the polling station at the polling place, if present.
810    pub fn polling_station_data_option(mut self, data: Option<impl Into<Box<str>>>) -> Self {
811        self.polling_station_data = data.map(|d| d.into());
812        self
813    }
814
815    /// Set the name of the locality of the polling place.
816    pub fn locality_name(mut self, locality_name: impl Into<LocalityName>) -> Self {
817        self.locality_name = Some(locality_name.into());
818        self
819    }
820
821    /// Set the postal code of the locality of the polling place.
822    pub fn postal_code(mut self, postal_code: impl Into<PostalCode>) -> Self {
823        self.postal_code = Some(postal_code.into());
824        self
825    }
826
827    /// Build the [`PollingPlace`], returning any errors if required fields are missing.
828    pub fn build(self) -> Result<PollingPlace, EMLError> {
829        Ok(PollingPlace {
830            channel: self
831                .channel
832                .ok_or(EMLErrorKind::MissingBuildProperty("channel").without_span())?,
833            physical_location: PhysicalLocation {
834                address: PhysicalLocationAddress {
835                    locality: PhysicalLocationLocality {
836                        locality_name: self.locality_name.ok_or(
837                            EMLErrorKind::MissingBuildProperty("locality_name").without_span(),
838                        )?,
839                        postal_code: self.postal_code,
840                    },
841                },
842                polling_station: PhysicalLocationPollingStation {
843                    id: self.polling_station_id.ok_or(
844                        EMLErrorKind::MissingBuildProperty("polling_station_id").without_span(),
845                    )?,
846                    data: self.polling_station_data.ok_or(
847                        EMLErrorKind::MissingBuildProperty("polling_station_data").without_span(),
848                    )?,
849                },
850            },
851        })
852    }
853}
854
855impl Default for PollingPlaceBuilder {
856    fn default() -> Self {
857        Self::new()
858    }
859}
860
861impl EMLElement for PollingPlace {
862    const EML_NAME: QualifiedName<'_, '_> =
863        QualifiedName::from_static("PollingPlace", Some(NS_EML));
864
865    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
866        Ok(collect_struct!(elem, PollingPlace {
867            physical_location: PhysicalLocation::EML_NAME => |elem| PhysicalLocation::read_eml(elem)?,
868            channel: elem.string_value_attr("Channel", None)?,
869        }))
870    }
871
872    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
873        writer
874            .attr("Channel", self.channel.raw().as_ref())?
875            .child_elem(PhysicalLocation::EML_NAME, &self.physical_location)?
876            .finish()
877    }
878}
879
880/// Physical location of a polling place.
881#[derive(Debug, Clone)]
882pub struct PhysicalLocation {
883    /// Address of the physical location.
884    pub address: PhysicalLocationAddress,
885
886    /// Polling station information of the physical location.
887    pub polling_station: PhysicalLocationPollingStation,
888}
889
890impl EMLElement for PhysicalLocation {
891    const EML_NAME: QualifiedName<'_, '_> =
892        QualifiedName::from_static("PhysicalLocation", Some(NS_EML));
893
894    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
895        Ok(collect_struct!(elem, PhysicalLocation {
896            address: PhysicalLocationAddress::EML_NAME => |elem| PhysicalLocationAddress::read_eml(elem)?,
897            polling_station: PhysicalLocationPollingStation::EML_NAME => |elem| PhysicalLocationPollingStation::read_eml(elem)?,
898        }))
899    }
900
901    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
902        writer
903            .child_elem(PhysicalLocationAddress::EML_NAME, &self.address)?
904            .child_elem(
905                PhysicalLocationPollingStation::EML_NAME,
906                &self.polling_station,
907            )?
908            .finish()
909    }
910}
911
912/// Address of a physical location.
913#[derive(Debug, Clone)]
914pub struct PhysicalLocationAddress {
915    /// Locality of the physical location.
916    pub locality: PhysicalLocationLocality,
917}
918
919impl EMLElement for PhysicalLocationAddress {
920    const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Address", Some(NS_EML));
921
922    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
923        Ok(collect_struct!(elem, PhysicalLocationAddress {
924            locality: PhysicalLocationLocality::EML_NAME => |elem| PhysicalLocationLocality::read_eml(elem)?,
925        }))
926    }
927
928    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
929        writer
930            .child_elem(PhysicalLocationLocality::EML_NAME, &self.locality)?
931            .finish()
932    }
933}
934
935/// Locality of a physical location.
936#[derive(Debug, Clone)]
937pub struct PhysicalLocationLocality {
938    /// Name of the locality.
939    pub locality_name: LocalityName,
940
941    /// Postal code of the locality.
942    pub postal_code: Option<PostalCode>,
943}
944
945impl EMLElement for PhysicalLocationLocality {
946    const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Locality", Some(NS_EML));
947
948    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
949        Ok(collect_struct!(elem, PhysicalLocationLocality {
950            locality_name: LocalityName::EML_NAME => |elem| LocalityName::read_eml(elem)?,
951            postal_code as Option: PostalCode::EML_NAME => |elem| PostalCode::read_eml(elem)?,
952        }))
953    }
954
955    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
956        writer
957            .child_elem(LocalityName::EML_NAME, &self.locality_name)?
958            .child_elem_option(PostalCode::EML_NAME, self.postal_code.as_ref())?
959            .finish()
960    }
961}
962
963/// Polling station information of a physical location.
964#[derive(Debug, Clone)]
965pub struct PhysicalLocationPollingStation {
966    /// Identifier of the polling station.
967    pub id: StringValue<PhysicalLocationPollingStationId>,
968
969    /// Additional data of the polling station.
970    pub data: Box<str>,
971}
972
973impl EMLElement for PhysicalLocationPollingStation {
974    const EML_NAME: QualifiedName<'_, '_> =
975        QualifiedName::from_static("PollingStation", Some(NS_EML));
976
977    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
978        Ok(PhysicalLocationPollingStation {
979            id: elem.string_value_attr("Id", None)?,
980            data: elem.text_without_children()?,
981        })
982    }
983
984    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
985        writer
986            .attr("Id", self.id.raw().as_ref())?
987            .text(self.data.as_ref())?
988            .finish()
989    }
990}
991
992/// Identifier for a physical location polling station.
993#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
994pub struct PhysicalLocationPollingStationId(u64);
995
996impl PhysicalLocationPollingStationId {
997    /// Create a new physical location polling station id from the given string, returning an error if the string is not a valid id.
998    pub fn new(s: impl AsRef<str>) -> Result<Self, EMLError> {
999        Self::parse_from_str(s.as_ref()).wrap_value_error()
1000    }
1001
1002    /// Get the value of the physical location polling station id as a u64.
1003    pub fn value(&self) -> u64 {
1004        self.0
1005    }
1006}
1007
1008impl From<u64> for PhysicalLocationPollingStationId {
1009    fn from(value: u64) -> Self {
1010        PhysicalLocationPollingStationId(value)
1011    }
1012}
1013
1014/// Error returned when a string could not be parsed as a PhysicalLocationPollingStationId
1015#[derive(Debug, Clone, Error)]
1016#[error("Invalid polling stations id: {0}")]
1017pub struct PhysicalLocationPollingStationIdError(String);
1018
1019/// Regular expression for validating ContestId values.
1020static PHYSICAL_LOCATION_PS_ID: LazyLock<Regex> = LazyLock::new(|| {
1021    Regex::new(r"^(\d+)$").expect("Failed to compile Physical Location Polling Station ID regex")
1022});
1023
1024impl StringValueData for PhysicalLocationPollingStationId {
1025    type Error = PhysicalLocationPollingStationIdError;
1026
1027    fn parse_from_str(s: &str) -> Result<Self, Self::Error>
1028    where
1029        Self: Sized,
1030    {
1031        if PHYSICAL_LOCATION_PS_ID.is_match(s) {
1032            Ok(PhysicalLocationPollingStationId(s.parse::<u64>().map_err(
1033                |_| PhysicalLocationPollingStationIdError(s.to_string()),
1034            )?))
1035        } else {
1036            Err(PhysicalLocationPollingStationIdError(s.to_string()))
1037        }
1038    }
1039
1040    fn to_raw_value(&self) -> Box<str> {
1041        self.0.to_string().into()
1042    }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use chrono::TimeZone as _;
1048
1049    use crate::{
1050        common::AuthorityIdentifier,
1051        io::{EMLParsingMode, EMLRead as _, EMLWrite as _},
1052        utils::{AuthorityId, ReportingUnitIdentifierId},
1053    };
1054
1055    use super::*;
1056
1057    #[test]
1058    fn test_physical_location_ps_id_regex_compiles() {
1059        LazyLock::force(&PHYSICAL_LOCATION_PS_ID);
1060    }
1061
1062    #[test]
1063    fn test_polling_stations_construction() {
1064        let ps = PollingStations::builder()
1065            .transaction_id(TransactionId::new(1))
1066            .managing_authority(
1067                AuthorityIdentifier::new(AuthorityId::new("1234").unwrap()).with_name("Test"),
1068            )
1069            .issue_date(XsDate::from_date(2024, 1, 1).unwrap())
1070            .creation_date_time(chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap())
1071            .election_identifier(
1072                PollingStationsElectionIdentifier::builder()
1073                    .id(ElectionId::new("TK2025").unwrap())
1074                    .name("Tweede Kamerverkiezingen 2025")
1075                    .category(ElectionCategory::TK)
1076                    .subcategory(ElectionSubcategory::TK)
1077                    .election_date(XsDate::from_date(2025, 3, 17).unwrap())
1078                    .build_for_polling_stations()
1079                    .unwrap(),
1080            )
1081            .contests([PollingStationsContest::builder()
1082                .reporting_unit(ReportingUnitIdentifier::new(
1083                    ReportingUnitIdentifierId::new("1234").unwrap(),
1084                    "Test",
1085                ))
1086                .max_votes(NonZeroU64::new(20).unwrap())
1087                .voting_method(VotingMethod::SPV)
1088                .polling_places([PollingPlace::builder()
1089                    .locality_name("Amsterdam")
1090                    .postal_code("1234 AB")
1091                    .channel(VotingChannelType::Polling)
1092                    .polling_station_data("123456")
1093                    .polling_station_id(PhysicalLocationPollingStationId::new("1234").unwrap())
1094                    .build()
1095                    .unwrap()])
1096                .build()
1097                .unwrap()])
1098            .build()
1099            .unwrap();
1100
1101        let xml = ps.write_eml_root_str(true, true).unwrap();
1102        assert_eq!(
1103            xml,
1104            include_str!(
1105                "../../test-files/polling_stations/eml110b_polling_stations_construction_output.eml.xml"
1106            )
1107        );
1108
1109        // check if it still is the same after a second parse and write
1110        let parsed = PollingStations::parse_eml(&xml, EMLParsingMode::Strict).unwrap();
1111        let xml2 = parsed.write_eml_root_str(true, true).unwrap();
1112        assert_eq!(xml, xml2);
1113    }
1114
1115    #[test]
1116    fn test_read_polling_stations_with_max_votes_empty() {
1117        let xml = include_str!(
1118            "../../test-files/polling_stations/eml110b_empty_number_of_voters.eml.xml"
1119        );
1120
1121        let parsed = PollingStations::parse_eml(xml, EMLParsingMode::Strict).unwrap();
1122        // empty MaxVotes should be parsed to 1, because 1 is the default value according to the EML standard
1123        assert_eq!(
1124            parsed.election_event.election.contests[0].max_votes,
1125            StringValue::Parsed(NonZeroU64::new(1).unwrap())
1126        );
1127    }
1128
1129    #[test]
1130    fn test_write_polling_stations_with_max_votes_empty() {
1131        let ps = PollingStations::builder()
1132            .transaction_id(TransactionId::new(1))
1133            .managing_authority(
1134                AuthorityIdentifier::new(AuthorityId::new("1234").unwrap()).with_name("Test"),
1135            )
1136            .issue_date(XsDate::from_date(2024, 1, 1).unwrap())
1137            .creation_date_time(chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap())
1138            .election_identifier(
1139                PollingStationsElectionIdentifier::builder()
1140                    .id(ElectionId::new("TK2025").unwrap())
1141                    .name("Tweede Kamerverkiezingen 2025")
1142                    .category(ElectionCategory::TK)
1143                    .subcategory(ElectionSubcategory::TK)
1144                    .election_date(XsDate::from_date(2025, 3, 17).unwrap())
1145                    .build_for_polling_stations()
1146                    .unwrap(),
1147            )
1148            .contests([PollingStationsContest::builder()
1149                .reporting_unit(ReportingUnitIdentifier::new(
1150                    ReportingUnitIdentifierId::new("1234").unwrap(),
1151                    "Test",
1152                ))
1153                .max_votes(NonZeroU64::new(1).unwrap())
1154                .voting_method(VotingMethod::SPV)
1155                .polling_places([PollingPlace::builder()
1156                    .locality_name("Amsterdam")
1157                    .postal_code("1234 AB")
1158                    .channel(VotingChannelType::Polling)
1159                    .polling_station_data("123456")
1160                    .polling_station_id(PhysicalLocationPollingStationId::new("1234").unwrap())
1161                    .build()
1162                    .unwrap()])
1163                .build()
1164                .unwrap()])
1165            .build()
1166            .unwrap();
1167
1168        let xml = ps.write_eml_root_str(true, true).unwrap();
1169        // max_votes of 1 should result in an empty MaxVotes tag, because 1 is the default value according to the EML standard
1170        assert!(xml.contains("<MaxVotes/>"))
1171    }
1172
1173    #[test]
1174    fn test_empty_polling_stations() {
1175        assert!(
1176            PollingStations::parse_eml(
1177                include_str!(
1178                    "../../test-files/polling_stations/eml110b_empty_polling_station.eml.xml"
1179                ),
1180                EMLParsingMode::Strict
1181            )
1182            .ok_with_errors()
1183            .is_err()
1184        )
1185    }
1186
1187    #[test]
1188    fn test_invalid_number_of_voters() {
1189        assert!(
1190            PollingStations::parse_eml(
1191                include_str!(
1192                    "../../test-files/polling_stations/eml110b_invalid_number_of_voters.eml.xml"
1193                ),
1194                EMLParsingMode::Strict
1195            )
1196            .ok_with_errors()
1197            .is_err()
1198        )
1199    }
1200
1201    #[test]
1202    fn test_one_station() {
1203        let ps = PollingStations::parse_eml(
1204            include_str!("../../test-files/polling_stations/eml110b_1_station.eml.xml"),
1205            EMLParsingMode::Strict,
1206        )
1207        .unwrap();
1208
1209        assert_eq!(ps.election_event.election.contests.len(), 1);
1210        let contest = &ps.election_event.election.contests[0];
1211        assert_eq!(contest.polling_places.len(), 1);
1212    }
1213
1214    #[test]
1215    fn test_less_than_10_stations() {
1216        let ps = PollingStations::parse_eml(
1217            include_str!("../../test-files/polling_stations/eml110b_less_than_10_stations.eml.xml"),
1218            EMLParsingMode::Strict,
1219        )
1220        .unwrap();
1221
1222        assert_eq!(ps.election_event.election.contests.len(), 1);
1223        let contest = &ps.election_event.election.contests[0];
1224        assert_eq!(contest.polling_places.len(), 9);
1225    }
1226}