1use 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#[derive(Debug, Clone)]
31pub struct PollingStations {
32 pub transaction_id: TransactionId,
34
35 pub managing_authority: ManagingAuthority,
37
38 pub issue_date: Option<IssueDate>,
40
41 pub creation_date_time: CreationDateTime,
43
44 pub canonicalization_method: Option<CanonicalizationMethod>,
46
47 pub election_event: PollingStationsElectionEvent,
49}
50
51impl PollingStations {
52 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#[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 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 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 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 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 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 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 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 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 pub fn contests(mut self, contests: impl Into<Vec<PollingStationsContest>>) -> Self {
179 self.contests = contests.into();
180 self
181 }
182
183 pub fn push_contest(mut self, contest: impl Into<PollingStationsContest>) -> Self {
188 self.contests.push(contest.into());
189 self
190 }
191
192 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 .child_elem(PollingStationsElectionEvent::EML_NAME, &self.election_event)?
272 .finish()?;
273
274 Ok(())
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct PollingStationsElectionEvent {
281 pub election: PollingStationsElection,
283}
284
285impl PollingStationsElectionEvent {
286 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#[derive(Debug, Clone)]
324pub struct PollingStationsElection {
325 pub identifier: PollingStationsElectionIdentifier,
327
328 pub contests: Vec<PollingStationsContest>,
330}
331
332impl PollingStationsElection {
333 pub fn new(identifier: impl Into<PollingStationsElectionIdentifier>) -> Self {
335 PollingStationsElection {
336 identifier: identifier.into(),
337 contests: vec![],
338 }
339 }
340
341 pub fn with_contests(mut self, contests: impl Into<Vec<PollingStationsContest>>) -> Self {
344 self.contests = contests.into();
345 self
346 }
347
348 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#[derive(Debug, Clone)]
381pub struct PollingStationsElectionIdentifier {
382 pub id: StringValue<ElectionId>,
384
385 pub name: Option<String>,
387
388 pub category: StringValue<ElectionCategory>,
390
391 pub subcategory: Option<StringValue<ElectionSubcategory>>,
393
394 pub domain: Option<ElectionDomain>,
396
397 pub election_date: StringValue<XsDate>,
399}
400
401impl PollingStationsElectionIdentifier {
402 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#[derive(Debug, Clone)]
494pub struct PollingStationsContest {
495 pub identifier: ContestIdentifierGeen,
497
498 pub reporting_unit: PollingStationsReportingUnit,
500
501 pub voting_method: StringValue<VotingMethod>,
503
504 pub max_votes: StringValue<NonZeroU64>,
506
507 pub polling_places: Vec<PollingPlace>,
509}
510
511impl PollingStationsContest {
512 pub fn builder() -> PollingStationsContestBuilder {
514 PollingStationsContestBuilder::new()
515 }
516}
517
518#[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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
712pub struct PollingStationsReportingUnit {
713 pub identifier: ReportingUnitIdentifier,
715}
716
717impl PollingStationsReportingUnit {
718 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#[derive(Debug, Clone)]
751pub struct PollingPlace {
752 pub channel: StringValue<VotingChannelType>,
754
755 pub physical_location: PhysicalLocation,
757}
758
759impl PollingPlace {
760 pub fn builder() -> PollingPlaceBuilder {
762 PollingPlaceBuilder::new()
763 }
764}
765
766#[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 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 pub fn channel(mut self, channel: impl Into<VotingChannelType>) -> Self {
790 self.channel = Some(StringValue::from_value(channel.into()));
791 self
792 }
793
794 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 pub fn polling_station_data(self, data: impl Into<String>) -> Self {
802 self.polling_station_data_option(Some(data))
803 }
804
805 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 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 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 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#[derive(Debug, Clone)]
878pub struct PhysicalLocation {
879 pub address: PhysicalLocationAddress,
881
882 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#[derive(Debug, Clone)]
910pub struct PhysicalLocationAddress {
911 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#[derive(Debug, Clone)]
933pub struct PhysicalLocationLocality {
934 pub locality_name: LocalityName,
936
937 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#[derive(Debug, Clone)]
961pub struct PhysicalLocationPollingStation {
962 pub id: StringValue<PhysicalLocationPollingStationId>,
964
965 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
990pub struct PhysicalLocationPollingStationId(u64);
991
992impl PhysicalLocationPollingStationId {
993 pub fn new(s: impl AsRef<str>) -> Result<Self, EMLError> {
995 Self::parse_from_str(s.as_ref()).wrap_value_error()
996 }
997
998 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#[derive(Debug, Clone, Error)]
1012#[error("Invalid polling stations id: {0}")]
1013pub struct PhysicalLocationPollingStationIdError(String);
1014
1015static 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 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}