use std::collections::{BTreeMap, HashSet};
use std::str::FromStr;
use thiserror::Error;
use crate::{
EMLError, EMLErrorKind, EMLVersion,
common::{Committee, RegionKey},
error::{EMLResultExt as _, EMLValueResultExt as _},
io::{
EMLElement, EMLElementReader, EMLElementWriter, EMLParsingMode, EMLWriteInternal as _,
QualifiedName, collect_struct,
},
utils::{CommitteeCategory, ElectionCategory, RegionCategory, StringValue, XsDateTime},
};
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum MasterElectionTreeError {
#[error("The root region of a master election tree must be of category STAAT, found {0:?}")]
RootNotState(RegionKey),
#[error(
"Region {region:?} has subregion {subregion:?}, which does not sit at a lower level in the election tree"
)]
InvalidSubregionCategory {
region: RegionKey,
subregion: RegionKey,
},
#[error("Region of category {0:?} is missing its RegionNumber attribute")]
MissingRegionNumber(RegionCategory),
#[error("Region {region:?} has more than one subregion with identity {subregion:?}")]
DuplicateSubregion {
region: RegionKey,
subregion: RegionKey,
},
#[error(
"Region {region:?} has more than one Council for election category {election_category:?}"
)]
DuplicateCouncil {
region: RegionKey,
election_category: ElectionCategory,
},
}
impl From<MasterElectionTreeError> for EMLError {
fn from(err: MasterElectionTreeError) -> Self {
EMLErrorKind::InvalidMasterElectionTree(err).without_span()
}
}
fn report_tree_error(
elem: &mut EMLElementReader<'_, '_>,
err: MasterElectionTreeError,
) -> Result<(), EMLError> {
let err = EMLErrorKind::InvalidMasterElectionTree(err).with_span(elem.full_span());
if elem.parsing_mode().is_strict() {
Err(err)
} else {
elem.push_err(err);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct MasterElectionTree {
pub creation_date: StringValue<XsDateTime>,
pub root: MetRegion,
}
impl MasterElectionTree {
pub fn new(creation_date: XsDateTime, root: MetRegion) -> Self {
MasterElectionTree {
creation_date: StringValue::from_value(creation_date),
root,
}
}
pub fn write_str(
&self,
pretty_print: bool,
include_declaration: bool,
) -> Result<String, EMLError> {
self.write_root_str(
Some(Self::EML_NAME),
Some(None),
Some(BTreeMap::new()),
EMLVersion::default(),
pretty_print,
include_declaration,
)
}
}
impl FromStr for MasterElectionTree {
type Err = EMLError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use crate::io::EMLRead as _;
Self::parse_eml_fragment(s, EMLParsingMode::Strict, EMLVersion::default()).ok()
}
}
impl TryFrom<&str> for MasterElectionTree {
type Error = EMLError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
use crate::io::EMLRead as _;
Self::parse_eml_fragment(value, EMLParsingMode::Strict, EMLVersion::default()).ok()
}
}
impl TryFrom<MasterElectionTree> for String {
type Error = EMLError;
fn try_from(value: MasterElectionTree) -> Result<Self, Self::Error> {
value.write_str(true, true)
}
}
impl EMLElement for MasterElectionTree {
const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("MasterElectionTree", None);
fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
if !elem.has_name(Self::EML_NAME)? {
return Err(EMLErrorKind::InvalidRootElement).with_span(elem.span());
}
let creation_date = elem.string_value_attr("CreationDate", None)?;
let tree = collect_struct!(elem, MasterElectionTree {
creation_date: creation_date,
root: MetRegion::EML_NAME => |elem| MetRegion::read_eml(elem)?,
});
if tree.root.key.category != RegionCategory::State {
report_tree_error(elem, MasterElectionTreeError::RootNotState(tree.root.key))?;
}
Ok(tree)
}
fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
writer
.attr("CreationDate", &self.creation_date.raw())?
.child_elem(MetRegion::EML_NAME, &self.root)?
.finish()
}
}
#[derive(Debug, Clone)]
pub struct MetRegion {
pub name: Box<str>,
pub key: RegionKey,
pub roman_numerals: bool,
pub frysian_export_allowed: bool,
pub councils: Vec<Council>,
pub committees: Vec<MetCommittee>,
pub subregions: Vec<MetRegion>,
}
impl MetRegion {
pub fn new(region_name: impl Into<Box<str>>, region_category: RegionCategory) -> Self {
MetRegion {
name: region_name.into(),
key: RegionKey::new(region_category, None),
roman_numerals: false,
frysian_export_allowed: false,
councils: Vec::new(),
committees: Vec::new(),
subregions: Vec::new(),
}
}
pub fn with_number(mut self, region_number: u16) -> Self {
self.key.number = Some(region_number);
self
}
pub fn with_roman_numerals(mut self, roman_numerals: bool) -> Self {
self.roman_numerals = roman_numerals;
self
}
pub fn with_frysian_export_allowed(mut self, enabled: bool) -> Self {
self.frysian_export_allowed = enabled;
self
}
pub fn with_councils(mut self, councils: Vec<Council>) -> Self {
self.councils = councils;
self
}
pub fn push_council(&mut self, council: Council) {
self.councils.push(council);
}
pub fn with_committees(mut self, committees: Vec<MetCommittee>) -> Self {
self.committees = committees;
self
}
pub fn push_committee(&mut self, committee: MetCommittee) {
self.committees.push(committee);
}
pub fn with_subregions(mut self, regions: Vec<MetRegion>) -> Self {
self.subregions = regions;
self
}
pub fn push_subregion(&mut self, region: MetRegion) {
self.subregions.push(region);
}
}
impl EMLElement for MetRegion {
const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Region", None);
fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
let number = elem
.attribute_value("RegionNumber")?
.map(|value| value.parse::<u16>())
.transpose()
.wrap_value_error()?;
let category = RegionCategory::new(elem.attribute_value_req("RegionCategory")?)?;
let roman_numerals = elem
.string_value_attr("RomanNumerals", Some("false"))?
.copied_value()?;
let frysian_export_allowed = elem
.string_value_attr("FrysianExportAllowed", Some("false"))?
.copied_value()?;
let region = collect_struct!(elem, MetRegion {
key: RegionKey::new(category, number),
roman_numerals: roman_numerals,
frysian_export_allowed: frysian_export_allowed,
name: ("RegionName", None) => |elem| elem.text_without_children()?,
councils as Vec: Council::EML_NAME => |elem| Council::read_eml(elem)?,
committees as Vec: MetCommittee::EML_NAME => |elem| MetCommittee::read_eml(elem)?,
subregions as Vec: MetRegion::EML_NAME => |elem| MetRegion::read_eml(elem)?,
});
if region.key.category != RegionCategory::State && region.key.number.is_none() {
report_tree_error(
elem,
MasterElectionTreeError::MissingRegionNumber(region.key.category),
)?;
}
let mut seen_subregions = HashSet::new();
for subregion in ®ion.subregions {
if !region
.key
.category
.is_higher_level_than(subregion.key.category)
{
report_tree_error(
elem,
MasterElectionTreeError::InvalidSubregionCategory {
region: region.key,
subregion: subregion.key,
},
)?;
}
if !seen_subregions.insert(subregion.key) {
report_tree_error(
elem,
MasterElectionTreeError::DuplicateSubregion {
region: region.key,
subregion: subregion.key,
},
)?;
}
}
let mut seen_councils = HashSet::new();
for council in ®ion.councils {
if !seen_councils.insert(council.election_category) {
report_tree_error(
elem,
MasterElectionTreeError::DuplicateCouncil {
region: region.key,
election_category: council.election_category,
},
)?;
}
}
Ok(region)
}
fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
writer
.attr_opt(
"RegionNumber",
self.key.number.map(|number| number.to_string()),
)?
.attr("RegionCategory", self.key.category.to_eml_value())?
.attr_opt("RomanNumerals", self.roman_numerals.then_some("true"))?
.attr_opt(
"FrysianExportAllowed",
self.frysian_export_allowed.then_some("true"),
)?
.content()?
.child(("RegionName", None), |writer| {
writer.text(self.name.as_ref())?.finish()
})?
.child_elems(Council::EML_NAME, &self.councils)?
.child_elems(MetCommittee::EML_NAME, &self.committees)?
.child_elems(MetRegion::EML_NAME, &self.subregions)?
.finish()
}
}
#[derive(Debug, Clone)]
pub struct Council {
pub election_category: ElectionCategory,
pub name: Option<Box<str>>,
pub number_of_seats: Option<StringValue<u64>>,
pub preference_threshold: Option<StringValue<u64>>,
}
impl Council {
pub fn new(election_category: ElectionCategory) -> Self {
Council {
election_category,
name: None,
number_of_seats: None,
preference_threshold: None,
}
}
pub fn with_name(mut self, name: impl Into<Box<str>>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_number_of_seats(mut self, number_of_seats: u64) -> Self {
self.number_of_seats = Some(StringValue::from_value(number_of_seats));
self
}
pub fn with_preference_threshold(mut self, preference_threshold: u64) -> Self {
self.preference_threshold = Some(StringValue::from_value(preference_threshold));
self
}
}
impl EMLElement for Council {
const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Council", None);
fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
Ok(Council {
election_category: ElectionCategory::new(
elem.attribute_value_req("ElectionCategory")?,
)?,
name: elem.attribute_value("CouncilName")?.map(|name| name.into()),
number_of_seats: elem.string_value_attr_opt("NumberOfSeats")?,
preference_threshold: elem.string_value_attr_opt("PreferenceThreshold")?,
})
}
fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
writer
.attr_opt(
"NumberOfSeats",
self.number_of_seats.as_ref().map(|v| v.raw()),
)?
.attr("ElectionCategory", self.election_category.to_eml_value())?
.attr_opt("CouncilName", self.name.as_ref())?
.attr_opt(
"PreferenceThreshold",
self.preference_threshold.as_ref().map(|v| v.raw()),
)?
.empty()
}
}
#[derive(Debug, Clone)]
pub struct MetCommittee {
pub election_category: ElectionCategory,
pub committee: Committee,
}
impl MetCommittee {
pub fn new(election_category: ElectionCategory, category: CommitteeCategory) -> Self {
MetCommittee {
election_category,
committee: Committee::new(category),
}
}
pub fn with_name(mut self, name: impl Into<Box<str>>) -> Self {
self.committee = self.committee.with_name(name);
self
}
pub fn with_accept_central_submissions(mut self, accept: bool) -> Self {
self.committee = self.committee.with_accept_central_submissions(accept);
self
}
}
impl EMLElement for MetCommittee {
const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Committee", None);
fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
Ok(MetCommittee {
election_category: ElectionCategory::new(
elem.attribute_value_req("ElectionCategory")?,
)?,
committee: crate::common::Committee::read_eml(elem)?,
})
}
fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
writer
.attr("CommitteeCategory", self.committee.category.to_eml_value())?
.attr("ElectionCategory", self.election_category.to_eml_value())?
.attr_opt("CommitteeName", self.committee.name.as_ref())?
.attr_opt(
"AcceptCentralSubmissions",
self.committee
.accept_central_submissions
.map(|value| value.to_string()),
)?
.empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::{EMLParsingMode, EMLRead as _, test_write_eml_element, test_xml_fragment};
#[test]
fn test_committee_parsing() {
let xml = test_xml_fragment(
r#"<Committee CommitteeCategory="HSB" ElectionCategory="EP" CommitteeName="De Kiesraad" AcceptCentralSubmissions="true"/>"#,
);
let committee =
MetCommittee::parse_eml_fragment(&xml, EMLParsingMode::Strict, EMLVersion::default())
.unwrap();
assert_eq!(committee.election_category, ElectionCategory::EP);
assert_eq!(committee.committee.category, CommitteeCategory::HSB);
assert_eq!(committee.committee.name, Some("De Kiesraad".into()));
assert_eq!(committee.committee.accept_central_submissions, Some(true));
let xml_output = test_write_eml_element(&committee, &[], EMLVersion::default()).unwrap();
assert_eq!(xml_output, xml);
}
#[test]
fn test_council_parsing() {
let xml = test_xml_fragment(
r#"<Council NumberOfSeats="150" ElectionCategory="TK" PreferenceThreshold="25"/>"#,
);
let council =
Council::parse_eml_fragment(&xml, EMLParsingMode::Strict, EMLVersion::default())
.unwrap();
assert_eq!(council.election_category, ElectionCategory::TK);
assert_eq!(council.name, None);
assert_eq!(council.number_of_seats, Some(StringValue::Parsed(150)));
assert_eq!(council.preference_threshold, Some(StringValue::Parsed(25)));
let xml_output = test_write_eml_element(&council, &[], EMLVersion::default()).unwrap();
assert_eq!(xml_output, xml);
}
#[test]
fn test_region_parsing() {
let xml = test_xml_fragment(
r#"
<Region RegionNumber="1" RegionCategory="PROVINCIE">
<RegionName>Groningen</RegionName>
<Committee CommitteeCategory="PROV_SB" ElectionCategory="EK"/>
<Region RegionNumber="14" RegionCategory="GEMEENTE">
<RegionName>Groningen</RegionName>
<Committee CommitteeCategory="CSB" ElectionCategory="GR"/>
</Region>
</Region>
"#,
);
let region =
MetRegion::parse_eml_fragment(&xml, EMLParsingMode::Strict, EMLVersion::default())
.unwrap();
assert_eq!(region.name.as_ref(), "Groningen");
assert_eq!(region.key, RegionKey::province(1));
assert!(!region.roman_numerals);
assert!(!region.frysian_export_allowed);
assert!(region.councils.is_empty());
assert_eq!(region.committees.len(), 1);
assert_eq!(
region.committees[0].committee.category,
CommitteeCategory::ProvSB
);
assert_eq!(region.subregions.len(), 1);
assert_eq!(region.subregions[0].name.as_ref(), "Groningen");
assert_eq!(region.subregions[0].key, RegionKey::municipality(14));
let xml_output = test_write_eml_element(®ion, &[], EMLVersion::default()).unwrap();
pretty_assertions::assert_eq!(xml_output, xml);
}
#[test]
fn test_region_rejects_subregion_of_invalid_category() {
let xml = test_xml_fragment(
r#"
<Region RegionNumber="14" RegionCategory="GEMEENTE">
<RegionName>Groningen</RegionName>
<Region RegionNumber="1" RegionCategory="PROVINCIE">
<RegionName>Groningen</RegionName>
</Region>
</Region>
"#,
);
let error =
MetRegion::parse_eml_fragment(&xml, EMLParsingMode::Strict, EMLVersion::default())
.ok()
.unwrap_err();
assert!(matches!(
error.kind(),
EMLErrorKind::InvalidMasterElectionTree(
MasterElectionTreeError::InvalidSubregionCategory { .. }
)
));
}
#[test]
fn test_region_rejects_missing_region_number() {
let xml = test_xml_fragment(
r#"
<Region RegionCategory="GEMEENTE">
<RegionName>Groningen</RegionName>
</Region>
"#,
);
let error =
MetRegion::parse_eml_fragment(&xml, EMLParsingMode::Strict, EMLVersion::default())
.ok()
.unwrap_err();
assert!(matches!(
error.kind(),
EMLErrorKind::InvalidMasterElectionTree(MasterElectionTreeError::MissingRegionNumber(
RegionCategory::Municipality
))
));
}
#[test]
fn test_region_rejects_duplicate_subregion() {
let xml = test_xml_fragment(
r#"
<Region RegionNumber="1" RegionCategory="PROVINCIE">
<RegionName>Groningen</RegionName>
<Region RegionNumber="14" RegionCategory="GEMEENTE">
<RegionName>Groningen</RegionName>
</Region>
<Region RegionNumber="14" RegionCategory="GEMEENTE">
<RegionName>Groningen</RegionName>
</Region>
</Region>
"#,
);
let error =
MetRegion::parse_eml_fragment(&xml, EMLParsingMode::Strict, EMLVersion::default())
.ok()
.unwrap_err();
assert!(matches!(
error.kind(),
EMLErrorKind::InvalidMasterElectionTree(
MasterElectionTreeError::DuplicateSubregion { .. }
)
));
}
#[test]
fn test_region_rejects_duplicate_council() {
let xml = test_xml_fragment(
r#"
<Region RegionCategory="STAAT">
<RegionName>Nederland</RegionName>
<Council NumberOfSeats="150" ElectionCategory="TK"/>
<Council NumberOfSeats="151" ElectionCategory="TK"/>
</Region>
"#,
);
let error =
MetRegion::parse_eml_fragment(&xml, EMLParsingMode::Strict, EMLVersion::default())
.ok()
.unwrap_err();
assert!(matches!(
error.kind(),
EMLErrorKind::InvalidMasterElectionTree(
MasterElectionTreeError::DuplicateCouncil { .. }
)
));
}
#[test]
fn test_master_election_tree_rejects_non_state_root() {
let xml = test_xml_fragment(
r#"
<MasterElectionTree CreationDate="2025-07-16T06:24:52+00:00">
<Region RegionNumber="1" RegionCategory="PROVINCIE">
<RegionName>Groningen</RegionName>
</Region>
</MasterElectionTree>
"#,
);
let error = MasterElectionTree::parse_eml_fragment(
&xml,
EMLParsingMode::Strict,
EMLVersion::default(),
)
.ok()
.unwrap_err();
assert!(matches!(
error.kind(),
EMLErrorKind::InvalidMasterElectionTree(MasterElectionTreeError::RootNotState(_))
));
}
#[test]
fn test_master_election_tree_parsing() {
let xml = test_xml_fragment(
r#"
<MasterElectionTree CreationDate="2025-07-16T06:24:52+00:00">
<Region RegionCategory="STAAT">
<RegionName>Nederland</RegionName>
<Council NumberOfSeats="150" ElectionCategory="TK"/>
</Region>
</MasterElectionTree>
"#,
);
let tree = MasterElectionTree::parse_eml_fragment(
&xml,
EMLParsingMode::Strict,
EMLVersion::default(),
)
.unwrap();
assert_eq!(tree.creation_date.raw(), "2025-07-16T06:24:52+00:00");
assert_eq!(tree.root.name.as_ref(), "Nederland");
assert_eq!(tree.root.key, RegionKey::state());
assert_eq!(tree.root.councils.len(), 1);
assert_eq!(
tree.root.councils[0].election_category,
ElectionCategory::TK
);
let xml_output = tree.write_str(true, false).unwrap();
pretty_assertions::assert_eq!(xml_output, xml);
}
#[test]
fn test_master_election_tree_full_fixture() {
let xml = include_str!("../../test-files/master_election_tree/MasterElectionTree.xml");
let tree = MasterElectionTree::parse_eml_fragment(
xml,
EMLParsingMode::Strict,
EMLVersion::default(),
)
.unwrap();
assert_eq!(tree.root.name.as_ref(), "Nederland");
assert_eq!(tree.root.key, RegionKey::state());
assert_eq!(tree.root.councils.len(), 3);
assert_eq!(tree.root.committees.len(), 2);
assert_eq!(tree.root.subregions.len(), 38);
fn count_regions(region: &MetRegion) -> usize {
1 + region.subregions.iter().map(count_regions).sum::<usize>()
}
assert_eq!(count_regions(&tree.root), 909);
let bonaire_kiescollege = tree
.root
.subregions
.iter()
.find(|r| r.key == RegionKey::new(RegionCategory::ElectoralCollege, Some(13)))
.unwrap();
assert_eq!(bonaire_kiescollege.name.as_ref(), "Bonaire");
let bonaire_kieskring = tree
.root
.subregions
.iter()
.find(|r| r.key == RegionKey::electoral_district(20))
.unwrap();
assert_eq!(bonaire_kieskring.name.as_ref(), "Bonaire");
assert_eq!(bonaire_kieskring.subregions.len(), 3);
assert_eq!(
bonaire_kieskring.subregions[0].key,
RegionKey::new(RegionCategory::IslandMunicipality, Some(9001))
);
}
#[test]
fn test_master_election_tree_round_trip() {
let xml = include_str!("../../test-files/master_election_tree/met_test.xml");
let tree = MasterElectionTree::parse_eml_fragment(
xml,
EMLParsingMode::Strict,
EMLVersion::default(),
)
.unwrap();
let xml_output = tree.write_str(true, true).unwrap();
pretty_assertions::assert_eq!(xml_output, xml);
}
}