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<Box<str>>,
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<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#[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>,
507
508 pub polling_places: Vec<PollingPlace>,
510}
511
512impl PollingStationsContest {
513 pub fn builder() -> PollingStationsContestBuilder {
515 PollingStationsContestBuilder::new()
516 }
517}
518
519#[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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
716pub struct PollingStationsReportingUnit {
717 pub identifier: ReportingUnitIdentifier,
719}
720
721impl PollingStationsReportingUnit {
722 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#[derive(Debug, Clone)]
755pub struct PollingPlace {
756 pub channel: StringValue<VotingChannelType>,
758
759 pub physical_location: PhysicalLocation,
761}
762
763impl PollingPlace {
764 pub fn builder() -> PollingPlaceBuilder {
766 PollingPlaceBuilder::new()
767 }
768}
769
770#[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 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 pub fn channel(mut self, channel: impl Into<VotingChannelType>) -> Self {
794 self.channel = Some(StringValue::from_value(channel.into()));
795 self
796 }
797
798 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 pub fn polling_station_data(self, data: impl Into<Box<str>>) -> Self {
806 self.polling_station_data_option(Some(data))
807 }
808
809 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 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 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 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#[derive(Debug, Clone)]
882pub struct PhysicalLocation {
883 pub address: PhysicalLocationAddress,
885
886 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#[derive(Debug, Clone)]
914pub struct PhysicalLocationAddress {
915 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#[derive(Debug, Clone)]
937pub struct PhysicalLocationLocality {
938 pub locality_name: LocalityName,
940
941 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#[derive(Debug, Clone)]
965pub struct PhysicalLocationPollingStation {
966 pub id: StringValue<PhysicalLocationPollingStationId>,
968
969 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
994pub struct PhysicalLocationPollingStationId(u64);
995
996impl PhysicalLocationPollingStationId {
997 pub fn new(s: impl AsRef<str>) -> Result<Self, EMLError> {
999 Self::parse_from_str(s.as_ref()).wrap_value_error()
1000 }
1001
1002 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#[derive(Debug, Clone, Error)]
1016#[error("Invalid polling stations id: {0}")]
1017pub struct PhysicalLocationPollingStationIdError(String);
1018
1019static 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 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 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 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}