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<String>,
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<String>,
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    pub max_votes: StringValue<NonZeroU64>,
506
507    /// List of polling places in this contest.
508    pub polling_places: Vec<PollingPlace>,
509}
510
511impl PollingStationsContest {
512    /// Create a new builder for constructing a [`PollingStationsContest`].
513    pub fn builder() -> PollingStationsContestBuilder {
514        PollingStationsContestBuilder::new()
515    }
516}
517
518/// Builder for the [`PollingStationsContest`] struct.
519#[derive(Debug, Clone)]
520pub struct PollingStationsContestBuilder {
521    reporting_unit: Option<PollingStationsReportingUnit>,
522    voting_method: Option<StringValue<VotingMethod>>,
523    max_votes: Option<StringValue<NonZeroU64>>,
524    polling_places: Vec<PollingPlace>,
525}
526
527impl PollingStationsContestBuilder {
528    /// Create a new builder for constructing a [`PollingStationsContest`].
529    pub fn new() -> Self {
530        Self {
531            reporting_unit: None,
532            voting_method: None,
533            max_votes: None,
534            polling_places: vec![],
535        }
536    }
537
538    /// Set the reporting unit for the contest.
539    pub fn reporting_unit(
540        mut self,
541        reporting_unit: impl Into<PollingStationsReportingUnit>,
542    ) -> Self {
543        self.reporting_unit = Some(reporting_unit.into());
544        self
545    }
546
547    /// Set the voting method used in the contest.
548    pub fn voting_method(mut self, voting_method: impl Into<VotingMethod>) -> Self {
549        self.voting_method = Some(StringValue::from_value(voting_method.into()));
550        self
551    }
552
553    /// Set the maximum number of votes allowed in the contest.
554    pub fn max_votes(mut self, max_votes: impl Into<NonZeroU64>) -> Self {
555        self.max_votes = Some(StringValue::from_value(max_votes.into()));
556        self
557    }
558
559    /// Set the list of polling places in the contest. This will replace any
560    /// existing polling places set using this method or the [`Self::push_polling_place`]
561    /// method.
562    pub fn polling_places(mut self, polling_places: impl Into<Vec<PollingPlace>>) -> Self {
563        self.polling_places = polling_places.into();
564        self
565    }
566
567    /// Add a polling place to the contest.
568    pub fn push_polling_place(mut self, polling_place: impl Into<PollingPlace>) -> Self {
569        self.polling_places.push(polling_place.into());
570        self
571    }
572
573    /// Build the [`PollingStationsContest`], returning any errors if required fields are missing.
574    pub fn build(self) -> Result<PollingStationsContest, EMLError> {
575        if self.polling_places.is_empty() {
576            return Err(EMLErrorKind::MissingBuildProperty("polling_places").without_span());
577        }
578
579        let voting_method = self
580            .voting_method
581            .ok_or(EMLErrorKind::MissingBuildProperty("voting_method").without_span())?;
582        if let Ok(vm) = voting_method.copied_value()
583            && vm != VotingMethod::SPV
584        {
585            return Err(EMLErrorKind::UnsupportedVotingMethod).without_span();
586        }
587
588        Ok(PollingStationsContest {
589            identifier: ContestIdentifierGeen::default(),
590            reporting_unit: self
591                .reporting_unit
592                .ok_or(EMLErrorKind::MissingBuildProperty("reporting_unit").without_span())?,
593            voting_method,
594            max_votes: self
595                .max_votes
596                .ok_or(EMLErrorKind::MissingBuildProperty("max_votes").without_span())?,
597            polling_places: self.polling_places,
598        })
599    }
600}
601
602impl Default for PollingStationsContestBuilder {
603    fn default() -> Self {
604        Self::new()
605    }
606}
607
608impl EMLElement for PollingStationsContest {
609    const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Contest", Some(NS_EML));
610
611    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
612        struct PollingStationsContestInternal {
613            pub identifier: Option<ContestIdentifierGeen>,
614            pub reporting_unit: PollingStationsReportingUnit,
615            pub voting_method: StringValue<VotingMethod>,
616            pub max_votes: StringValue<NonZeroU64>,
617            pub polling_places: Vec<PollingPlace>,
618        }
619
620        let data = collect_struct!(elem, PollingStationsContestInternal {
621            identifier as Option: ContestIdentifierGeen::EML_NAME => |elem| ContestIdentifierGeen::read_eml(elem)?,
622            reporting_unit: PollingStationsReportingUnit::EML_NAME => |elem| PollingStationsReportingUnit::read_eml(elem)?,
623            voting_method: ("VotingMethod", NS_EML) => |elem| {
624                let value = elem.string_value_opt()?;
625                if let Some(value) = value {
626                    value
627                } else {
628                    let err = EMLErrorKind::MissingElementValue(OwnedQualifiedName::from_static("VotingMethod", Some(NS_EML)))
629                        .with_span(elem.full_span());
630                    if elem.parsing_mode().is_strict() {
631                        return Err(err);
632                    } else {
633                        elem.push_err(err);
634                        StringValue::from_value(VotingMethod::SPV)
635                    }
636                }
637            },
638            max_votes: ("MaxVotes", NS_EML) => |elem| {
639                let text = elem.text_without_children_opt()?.unwrap_or_else(|| "1".to_string());
640                elem.string_value_from_text(text, None, elem.full_span())?
641            },
642            polling_places as Vec: PollingPlace::EML_NAME => |elem| PollingPlace::read_eml(elem)?,
643        });
644
645        // Some municipalities omit the ContestIdentifier element, even though it is required.
646        let identifier = if let Some(identifier) = data.identifier {
647            identifier
648        } else {
649            let err = EMLErrorKind::MissingContenstIdentifier.with_span(elem.span());
650            if elem.parsing_mode().is_strict() {
651                return Err(err);
652            } else {
653                elem.push_err(err);
654                ContestIdentifierGeen::default()
655            }
656        };
657
658        // Technically there should be at least one polling place
659        if data.polling_places.is_empty() {
660            let err = EMLErrorKind::MissingElement(PollingPlace::EML_NAME.as_owned())
661                .with_span(elem.full_span());
662            if elem.parsing_mode().is_strict() {
663                return Err(err);
664            } else {
665                elem.push_err(err);
666            }
667        }
668
669        // Only SPV voting method is supported
670        if let Ok(vm) = data.voting_method.copied_value()
671            && vm != VotingMethod::SPV
672        {
673            let err = EMLErrorKind::UnsupportedVotingMethod.with_span(elem.full_span());
674            if elem.parsing_mode().is_strict() {
675                return Err(err);
676            } else {
677                elem.push_err(err);
678            }
679        }
680
681        Ok(PollingStationsContest {
682            identifier,
683            reporting_unit: data.reporting_unit,
684            voting_method: data.voting_method,
685            max_votes: data.max_votes,
686            polling_places: data.polling_places,
687        })
688    }
689
690    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
691        writer
692            .child_elem(ContestIdentifier::EML_NAME, &self.identifier)?
693            .child_elem(PollingStationsReportingUnit::EML_NAME, &self.reporting_unit)?
694            .child(("VotingMethod", NS_EML), |elem| {
695                elem.text(self.voting_method.raw().as_ref())?.finish()
696            })?
697            .child(("MaxVotes", NS_EML), |elem| {
698                let raw_text = self.max_votes.raw();
699                if raw_text == "1" {
700                    elem.empty()
701                } else {
702                    elem.text(raw_text.as_ref())?.finish()
703                }
704            })?
705            .child_elems(PollingPlace::EML_NAME, &self.polling_places)?
706            .finish()
707    }
708}
709
710/// Reporting unit for the contest
711#[derive(Debug, Clone)]
712pub struct PollingStationsReportingUnit {
713    /// Identifier of the reporting unit.
714    pub identifier: ReportingUnitIdentifier,
715}
716
717impl PollingStationsReportingUnit {
718    /// Create a new reporting unit for the polling stations document.
719    pub fn new(identifier: impl Into<ReportingUnitIdentifier>) -> Self {
720        PollingStationsReportingUnit {
721            identifier: identifier.into(),
722        }
723    }
724}
725
726impl From<ReportingUnitIdentifier> for PollingStationsReportingUnit {
727    fn from(value: ReportingUnitIdentifier) -> Self {
728        PollingStationsReportingUnit::new(value)
729    }
730}
731
732impl EMLElement for PollingStationsReportingUnit {
733    const EML_NAME: QualifiedName<'_, '_> =
734        QualifiedName::from_static("ReportingUnit", Some(NS_EML));
735
736    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
737        Ok(collect_struct!(elem, PollingStationsReportingUnit {
738            identifier: ReportingUnitIdentifier::EML_NAME => |elem| ReportingUnitIdentifier::read_eml(elem)?,
739        }))
740    }
741
742    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
743        writer
744            .child_elem(ReportingUnitIdentifier::EML_NAME, &self.identifier)?
745            .finish()
746    }
747}
748
749/// A polling place in the polling stations document.
750#[derive(Debug, Clone)]
751pub struct PollingPlace {
752    /// Voting channel used at this polling place.
753    pub channel: StringValue<VotingChannelType>,
754
755    /// Physical location of the polling place.
756    pub physical_location: PhysicalLocation,
757}
758
759impl PollingPlace {
760    /// Create a new builder for constructing a [`PollingPlace`].
761    pub fn builder() -> PollingPlaceBuilder {
762        PollingPlaceBuilder::new()
763    }
764}
765
766/// Builder for the [`PollingPlace`] struct.
767#[derive(Debug, Clone)]
768pub struct PollingPlaceBuilder {
769    channel: Option<StringValue<VotingChannelType>>,
770    polling_station_id: Option<StringValue<PhysicalLocationPollingStationId>>,
771    polling_station_data: Option<String>,
772    locality_name: Option<LocalityName>,
773    postal_code: Option<PostalCode>,
774}
775
776impl PollingPlaceBuilder {
777    /// Create a new builder for constructing a [`PollingPlace`].
778    pub fn new() -> Self {
779        Self {
780            channel: None,
781            polling_station_id: None,
782            polling_station_data: None,
783            locality_name: None,
784            postal_code: None,
785        }
786    }
787
788    /// Set the voting channel used at the polling place.
789    pub fn channel(mut self, channel: impl Into<VotingChannelType>) -> Self {
790        self.channel = Some(StringValue::from_value(channel.into()));
791        self
792    }
793
794    /// Set the identifier of the polling station at the polling place.
795    pub fn polling_station_id(mut self, id: impl Into<PhysicalLocationPollingStationId>) -> Self {
796        self.polling_station_id = Some(StringValue::from_value(id.into()));
797        self
798    }
799
800    /// Set the additional data of the polling station at the polling place.
801    pub fn polling_station_data(self, data: impl Into<String>) -> Self {
802        self.polling_station_data_option(Some(data))
803    }
804
805    /// Set the additional data of the polling station at the polling place, if present.
806    pub fn polling_station_data_option(mut self, data: Option<impl Into<String>>) -> Self {
807        self.polling_station_data = data.map(|d| d.into());
808        self
809    }
810
811    /// Set the name of the locality of the polling place.
812    pub fn locality_name(mut self, locality_name: impl Into<LocalityName>) -> Self {
813        self.locality_name = Some(locality_name.into());
814        self
815    }
816
817    /// Set the postal code of the locality of the polling place.
818    pub fn postal_code(mut self, postal_code: impl Into<PostalCode>) -> Self {
819        self.postal_code = Some(postal_code.into());
820        self
821    }
822
823    /// Build the [`PollingPlace`], returning any errors if required fields are missing.
824    pub fn build(self) -> Result<PollingPlace, EMLError> {
825        Ok(PollingPlace {
826            channel: self
827                .channel
828                .ok_or(EMLErrorKind::MissingBuildProperty("channel").without_span())?,
829            physical_location: PhysicalLocation {
830                address: PhysicalLocationAddress {
831                    locality: PhysicalLocationLocality {
832                        locality_name: self.locality_name.ok_or(
833                            EMLErrorKind::MissingBuildProperty("locality_name").without_span(),
834                        )?,
835                        postal_code: self.postal_code,
836                    },
837                },
838                polling_station: PhysicalLocationPollingStation {
839                    id: self.polling_station_id.ok_or(
840                        EMLErrorKind::MissingBuildProperty("polling_station_id").without_span(),
841                    )?,
842                    data: self.polling_station_data.ok_or(
843                        EMLErrorKind::MissingBuildProperty("polling_station_data").without_span(),
844                    )?,
845                },
846            },
847        })
848    }
849}
850
851impl Default for PollingPlaceBuilder {
852    fn default() -> Self {
853        Self::new()
854    }
855}
856
857impl EMLElement for PollingPlace {
858    const EML_NAME: QualifiedName<'_, '_> =
859        QualifiedName::from_static("PollingPlace", Some(NS_EML));
860
861    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
862        Ok(collect_struct!(elem, PollingPlace {
863            physical_location: PhysicalLocation::EML_NAME => |elem| PhysicalLocation::read_eml(elem)?,
864            channel: elem.string_value_attr("Channel", None)?,
865        }))
866    }
867
868    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
869        writer
870            .attr("Channel", self.channel.raw().as_ref())?
871            .child_elem(PhysicalLocation::EML_NAME, &self.physical_location)?
872            .finish()
873    }
874}
875
876/// Physical location of a polling place.
877#[derive(Debug, Clone)]
878pub struct PhysicalLocation {
879    /// Address of the physical location.
880    pub address: PhysicalLocationAddress,
881
882    /// Polling station information of the physical location.
883    pub polling_station: PhysicalLocationPollingStation,
884}
885
886impl EMLElement for PhysicalLocation {
887    const EML_NAME: QualifiedName<'_, '_> =
888        QualifiedName::from_static("PhysicalLocation", Some(NS_EML));
889
890    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
891        Ok(collect_struct!(elem, PhysicalLocation {
892            address: PhysicalLocationAddress::EML_NAME => |elem| PhysicalLocationAddress::read_eml(elem)?,
893            polling_station: PhysicalLocationPollingStation::EML_NAME => |elem| PhysicalLocationPollingStation::read_eml(elem)?,
894        }))
895    }
896
897    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
898        writer
899            .child_elem(PhysicalLocationAddress::EML_NAME, &self.address)?
900            .child_elem(
901                PhysicalLocationPollingStation::EML_NAME,
902                &self.polling_station,
903            )?
904            .finish()
905    }
906}
907
908/// Address of a physical location.
909#[derive(Debug, Clone)]
910pub struct PhysicalLocationAddress {
911    /// Locality of the physical location.
912    pub locality: PhysicalLocationLocality,
913}
914
915impl EMLElement for PhysicalLocationAddress {
916    const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Address", Some(NS_EML));
917
918    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
919        Ok(collect_struct!(elem, PhysicalLocationAddress {
920            locality: PhysicalLocationLocality::EML_NAME => |elem| PhysicalLocationLocality::read_eml(elem)?,
921        }))
922    }
923
924    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
925        writer
926            .child_elem(PhysicalLocationLocality::EML_NAME, &self.locality)?
927            .finish()
928    }
929}
930
931/// Locality of a physical location.
932#[derive(Debug, Clone)]
933pub struct PhysicalLocationLocality {
934    /// Name of the locality.
935    pub locality_name: LocalityName,
936
937    /// Postal code of the locality.
938    pub postal_code: Option<PostalCode>,
939}
940
941impl EMLElement for PhysicalLocationLocality {
942    const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Locality", Some(NS_EML));
943
944    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
945        Ok(collect_struct!(elem, PhysicalLocationLocality {
946            locality_name: LocalityName::EML_NAME => |elem| LocalityName::read_eml(elem)?,
947            postal_code as Option: PostalCode::EML_NAME => |elem| PostalCode::read_eml(elem)?,
948        }))
949    }
950
951    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
952        writer
953            .child_elem(LocalityName::EML_NAME, &self.locality_name)?
954            .child_elem_option(PostalCode::EML_NAME, self.postal_code.as_ref())?
955            .finish()
956    }
957}
958
959/// Polling station information of a physical location.
960#[derive(Debug, Clone)]
961pub struct PhysicalLocationPollingStation {
962    /// Identifier of the polling station.
963    pub id: StringValue<PhysicalLocationPollingStationId>,
964
965    /// Additional data of the polling station.
966    pub data: String,
967}
968
969impl EMLElement for PhysicalLocationPollingStation {
970    const EML_NAME: QualifiedName<'_, '_> =
971        QualifiedName::from_static("PollingStation", Some(NS_EML));
972
973    fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
974        Ok(PhysicalLocationPollingStation {
975            id: elem.string_value_attr("Id", None)?,
976            data: elem.text_without_children()?,
977        })
978    }
979
980    fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
981        writer
982            .attr("Id", self.id.raw().as_ref())?
983            .text(self.data.as_ref())?
984            .finish()
985    }
986}
987
988/// Identifier for a physical location polling station.
989#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
990pub struct PhysicalLocationPollingStationId(u64);
991
992impl PhysicalLocationPollingStationId {
993    /// Create a new physical location polling station id from the given string, returning an error if the string is not a valid id.
994    pub fn new(s: impl AsRef<str>) -> Result<Self, EMLError> {
995        Self::parse_from_str(s.as_ref()).wrap_value_error()
996    }
997
998    /// Get the value of the physical location polling station id as a u64.
999    pub fn value(&self) -> u64 {
1000        self.0
1001    }
1002}
1003
1004impl From<u64> for PhysicalLocationPollingStationId {
1005    fn from(value: u64) -> Self {
1006        PhysicalLocationPollingStationId(value)
1007    }
1008}
1009
1010/// Error returned when a string could not be parsed as a PhysicalLocationPollingStationId
1011#[derive(Debug, Clone, Error)]
1012#[error("Invalid polling stations id: {0}")]
1013pub struct PhysicalLocationPollingStationIdError(String);
1014
1015/// Regular expression for validating ContestId values.
1016static PHYSICAL_LOCATION_PS_ID: LazyLock<Regex> = LazyLock::new(|| {
1017    Regex::new(r"^(\d+)$").expect("Failed to compile Physical Location Polling Station ID regex")
1018});
1019
1020impl StringValueData for PhysicalLocationPollingStationId {
1021    type Error = PhysicalLocationPollingStationIdError;
1022
1023    fn parse_from_str(s: &str) -> Result<Self, Self::Error>
1024    where
1025        Self: Sized,
1026    {
1027        if PHYSICAL_LOCATION_PS_ID.is_match(s) {
1028            Ok(PhysicalLocationPollingStationId(s.parse::<u64>().map_err(
1029                |_| PhysicalLocationPollingStationIdError(s.to_string()),
1030            )?))
1031        } else {
1032            Err(PhysicalLocationPollingStationIdError(s.to_string()))
1033        }
1034    }
1035
1036    fn to_raw_value(&self) -> String {
1037        self.0.to_string()
1038    }
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043    use chrono::TimeZone as _;
1044
1045    use crate::{
1046        common::AuthorityIdentifier,
1047        io::{EMLParsingMode, EMLRead as _, EMLWrite as _},
1048        utils::{AuthorityId, ReportingUnitIdentifierId},
1049    };
1050
1051    use super::*;
1052
1053    #[test]
1054    fn test_physical_location_ps_id_regex_compiles() {
1055        LazyLock::force(&PHYSICAL_LOCATION_PS_ID);
1056    }
1057
1058    #[test]
1059    fn test_polling_stations_construction() {
1060        let ps = PollingStations::builder()
1061            .transaction_id(TransactionId::new(1))
1062            .managing_authority(
1063                AuthorityIdentifier::new(AuthorityId::new("1234").unwrap()).with_name("Test"),
1064            )
1065            .issue_date(XsDate::from_date(2024, 1, 1).unwrap())
1066            .creation_date_time(chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap())
1067            .election_identifier(
1068                PollingStationsElectionIdentifier::builder()
1069                    .id(ElectionId::new("TK2025").unwrap())
1070                    .name("Tweede Kamerverkiezingen 2025")
1071                    .category(ElectionCategory::TK)
1072                    .subcategory(ElectionSubcategory::TK)
1073                    .election_date(XsDate::from_date(2025, 3, 17).unwrap())
1074                    .build_for_polling_stations()
1075                    .unwrap(),
1076            )
1077            .contests([PollingStationsContest::builder()
1078                .reporting_unit(ReportingUnitIdentifier::new(
1079                    ReportingUnitIdentifierId::new("1234").unwrap(),
1080                    "Test",
1081                ))
1082                .max_votes(NonZeroU64::new(20).unwrap())
1083                .voting_method(VotingMethod::SPV)
1084                .polling_places([PollingPlace::builder()
1085                    .locality_name("Amsterdam")
1086                    .postal_code("1234 AB")
1087                    .channel(VotingChannelType::Polling)
1088                    .polling_station_data("123456")
1089                    .polling_station_id(PhysicalLocationPollingStationId::new("1234").unwrap())
1090                    .build()
1091                    .unwrap()])
1092                .build()
1093                .unwrap()])
1094            .build()
1095            .unwrap();
1096
1097        let xml = ps.write_eml_root_str(true, true).unwrap();
1098        assert_eq!(
1099            xml,
1100            include_str!(
1101                "../../test-emls/polling_stations/eml110b_polling_stations_construction_output.eml.xml"
1102            )
1103        );
1104
1105        // check if it still is the same after a second parse and write
1106        let parsed = PollingStations::parse_eml(&xml, EMLParsingMode::Strict).unwrap();
1107        let xml2 = parsed.write_eml_root_str(true, true).unwrap();
1108        assert_eq!(xml, xml2);
1109    }
1110
1111    #[test]
1112    fn test_empty_polling_stations() {
1113        assert!(
1114            PollingStations::parse_eml(
1115                include_str!(
1116                    "../../test-emls/polling_stations/eml110b_empty_polling_station.eml.xml"
1117                ),
1118                EMLParsingMode::Strict
1119            )
1120            .ok_with_errors()
1121            .is_err()
1122        )
1123    }
1124
1125    #[test]
1126    fn test_invalid_number_of_voters() {
1127        assert!(
1128            PollingStations::parse_eml(
1129                include_str!(
1130                    "../../test-emls/polling_stations/eml110b_invalid_number_of_voters.eml.xml"
1131                ),
1132                EMLParsingMode::Strict
1133            )
1134            .ok_with_errors()
1135            .is_err()
1136        )
1137    }
1138
1139    #[test]
1140    fn test_one_station() {
1141        let ps = PollingStations::parse_eml(
1142            include_str!("../../test-emls/polling_stations/eml110b_1_station.eml.xml"),
1143            EMLParsingMode::Strict,
1144        )
1145        .unwrap();
1146
1147        assert_eq!(ps.election_event.election.contests.len(), 1);
1148        let contest = &ps.election_event.election.contests[0];
1149        assert_eq!(contest.polling_places.len(), 1);
1150    }
1151
1152    #[test]
1153    fn test_less_than_10_stations() {
1154        let ps = PollingStations::parse_eml(
1155            include_str!("../../test-emls/polling_stations/eml110b_less_than_10_stations.eml.xml"),
1156            EMLParsingMode::Strict,
1157        )
1158        .unwrap();
1159
1160        assert_eq!(ps.election_event.election.contests.len(), 1);
1161        let contest = &ps.election_event.election.contests[0];
1162        assert_eq!(contest.polling_places.len(), 9);
1163    }
1164}